diff --git a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets index 668ea155a0..5564cdb840 100644 --- a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets +++ b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets @@ -349,6 +349,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. AdditionalArguments="@(CsWinRTGeneratorAdditionalArgument)" StandardOutputImportance="$(CsWinRTGeneratorStandardOutputImportance)" StandardErrorImportance="$(CsWinRTGeneratorStandardErrorImportance)" + MaxDegreesOfParallelism="$(CsWinRTGeneratorMaxDegreesOfParallelism)" LogStandardErrorAsError="$(CsWinRTGeneratorLogStandardErrorAsError)" /> @@ -411,6 +412,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. CsWinRTToolsArchitecture="$(CsWinRTToolsArchitecture)" StandardOutputImportance="$(CsWinRTGeneratorStandardOutputImportance)" StandardErrorImportance="$(CsWinRTGeneratorStandardErrorImportance)" + MaxDegreesOfParallelism="$(CsWinRTGeneratorMaxDegreesOfParallelism)" LogStandardErrorAsError="$(CsWinRTGeneratorLogStandardErrorAsError)" /> @@ -488,6 +490,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. AdditionalArguments="@(CsWinRTGeneratorAdditionalArgument)" StandardOutputImportance="$(CsWinRTGeneratorStandardOutputImportance)" StandardErrorImportance="$(CsWinRTGeneratorStandardErrorImportance)" + MaxDegreesOfParallelism="$(CsWinRTGeneratorMaxDegreesOfParallelism)" LogStandardErrorAsError="$(CsWinRTGeneratorLogStandardErrorAsError)" /> @@ -573,6 +576,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. AdditionalArguments="@(CsWinRTGeneratorAdditionalArgument)" StandardOutputImportance="$(CsWinRTGeneratorStandardOutputImportance)" StandardErrorImportance="$(CsWinRTGeneratorStandardErrorImportance)" + MaxDegreesOfParallelism="$(CsWinRTGeneratorMaxDegreesOfParallelism)" LogStandardErrorAsError="$(CsWinRTGeneratorLogStandardErrorAsError)" /> diff --git a/src/Tests/CSWinMDComponent/CSWinMDComponent.vcxproj.filters b/src/Tests/CSWinMDComponent/CSWinMDComponent.vcxproj.filters new file mode 100644 index 0000000000..c1f12129b1 --- /dev/null +++ b/src/Tests/CSWinMDComponent/CSWinMDComponent.vcxproj.filters @@ -0,0 +1,32 @@ + + + + + accd3aa8-1ba0-4223-9bbe-0c431709210b + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {926ab91d-31b4-48c3-b9a4-e681349f27f0} + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTMergedProjectionGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTMergedProjectionGenerator.cs index 307304eda4..f8c09e003f 100644 --- a/src/WinRT.Generator.Tasks/RunCsWinRTMergedProjectionGenerator.cs +++ b/src/WinRT.Generator.Tasks/RunCsWinRTMergedProjectionGenerator.cs @@ -86,6 +86,12 @@ public sealed class RunCsWinRTMergedProjectionGenerator : ToolTask /// public bool WindowsUIXamlProjection { get; set; } + /// + /// Gets or sets the maximum number of parallel tasks the projection writer is allowed to dispatch. + /// + /// If not set, the default will match the number of available processor cores. + public int MaxDegreesOfParallelism { get; set; } = -1; + /// protected override string ToolName => "cswinrtprojectiongen.exe"; @@ -208,6 +214,8 @@ protected override string GenerateResponseFileCommands() AppendResponseFileCommand(args, "--windows-ui-xaml-projection", "true"); } + AppendResponseFileCommand(args, "--max-degrees-of-parallelism", MaxDegreesOfParallelism.ToString()); + // Add any additional arguments that are not statically known foreach (ITaskItem additionalArgument in AdditionalArguments ?? []) { diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs index 46a03b6d76..2d7ea67300 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropException.cs @@ -10,7 +10,7 @@ namespace WindowsRuntime.InteropGenerator.Errors; /// -/// A well known exception for the interop generator. +/// A well-known exception for the interop generator. /// internal sealed class WellKnownInteropException : Exception { diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropWarning.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropWarning.cs index acab2a833e..ae8da9ef17 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropWarning.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropWarning.cs @@ -8,7 +8,7 @@ namespace WindowsRuntime.InteropGenerator.Errors; /// -/// A well known warning for the interop generator. +/// A well-known warning for the interop generator. /// internal sealed class WellKnownInteropWarning { 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 deleted file mode 100644 index bcef2c24fc..0000000000 --- a/src/WinRT.Projection.Generator.Writer.TestRunner/WinRT.Projection.Generator.Writer.TestRunner.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - 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 deleted file mode 100644 index 422d99afe7..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Generation/ProjectionGenerator.cs +++ /dev/null @@ -1,417 +0,0 @@ -// 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 deleted file mode 100644 index 168ffd98b0..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/Additions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index 536f7d88e9..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/AttributedTypes.cs +++ /dev/null @@ -1,116 +0,0 @@ -// 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 deleted file mode 100644 index e21c0a4bd7..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/ContractPlatforms.cs +++ /dev/null @@ -1,150 +0,0 @@ -// 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 deleted file mode 100644 index 0535031628..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/GuidGenerator.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index 63271bad1b..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/Helpers.cs +++ /dev/null @@ -1,326 +0,0 @@ -// 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/Settings.cs b/src/WinRT.Projection.Generator.Writer/Helpers/Settings.cs deleted file mode 100644 index 1246a17de8..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/Settings.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index 4a3d7bde12..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Helpers/TypeFilter.cs +++ /dev/null @@ -1,139 +0,0 @@ -// 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/Metadata/TypeSemantics.cs b/src/WinRT.Projection.Generator.Writer/Metadata/TypeSemantics.cs deleted file mode 100644 index 8cac4af294..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Metadata/TypeSemantics.cs +++ /dev/null @@ -1,192 +0,0 @@ -// 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 deleted file mode 100644 index f16052a3c4..0000000000 --- a/src/WinRT.Projection.Generator.Writer/ProjectionWriter.cs +++ /dev/null @@ -1,66 +0,0 @@ -// 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 deleted file mode 100644 index b072180615..0000000000 --- a/src/WinRT.Projection.Generator.Writer/ProjectionWriterOptions.cs +++ /dev/null @@ -1,68 +0,0 @@ -// 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/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Abi.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Abi.cs deleted file mode 100644 index 28c420d5b2..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Abi.cs +++ /dev/null @@ -1,6346 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// ABI emission helpers (structs, enums, delegates, interfaces, classes). -/// Mirrors the C++ write_abi_* family. Initial port: emits the foundational -/// ABI scaffolding only; full marshaller/vtable emission to be filled in later. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ is_type_blittable partially. - public static bool IsTypeBlittable(TypeDefinition type) - { - TypeCategory cat = TypeCategorization.GetCategory(type); - if (cat == TypeCategory.Enum) { return true; } - if (cat != TypeCategory.Struct) { return false; } - // Mirrors C++ is_type_blittable (code_writers.h:81-124, struct_type branch): if the - // struct itself has a mapped-type entry, return based on its RequiresMarshaling flag - // BEFORE walking fields. This is critical for XAML structs like Duration / KeyTime / - // RepeatBehavior which are self-mapped with RequiresMarshaling=false but have a - // TimeSpan field (Windows.Foundation.TimeSpan -> System.TimeSpan with RequiresMarshaling=true). - // Without this check, the field walk would incorrectly classify them as non-blittable. - string ns = type.Namespace?.Value ?? string.Empty; - string name = type.Name?.Value ?? string.Empty; - if (MappedTypes.Get(ns, name) is { } mapping) - { - return !mapping.RequiresMarshaling; - } - // Walk fields - all must be blittable - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - if (!IsFieldTypeBlittable(field.Signature.FieldType)) { return false; } - } - return true; - } - - private static bool IsFieldTypeBlittable(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) - { - // Mirror C++ is_type_blittable for fundamental_type: - // return (type != fundamental_type::String); - // i.e. ALL fundamentals (including Boolean, Char) are considered blittable here; - // only String is non-blittable. Object isn't a fundamental in C++; handled below. - return corlib.ElementType switch - { - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String => false, - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object => false, - _ => true - }; - } - // For TypeRef/TypeDef, resolve and check blittability. - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature todr) - { - string fNs = todr.Type?.Namespace?.Value ?? string.Empty; - string fName = todr.Type?.Name?.Value ?? string.Empty; - // System.Guid is a fundamental blittable type (mirrors C++ guid_type which falls - // through to the [&](auto&&) catch-all returning true in is_type_blittable). - // Same applies to System.IntPtr / UIntPtr (used in some struct layouts). - if (fNs == "System" && (fName == "Guid" || fName == "IntPtr" || fName == "UIntPtr")) - { - return true; - } - // Mapped struct types: blittable iff the mapping does NOT require marshalling - // (mirrors C++ is_type_blittable for mapped struct_type case). - MappedType? mapped = MappedTypes.Get(fNs, fName); - if (mapped is not null && mapped.RequiresMarshaling) { return false; } - if (todr.Type is TypeDefinition td) - { - return IsTypeBlittable(td); - } - // Cross-module: try metadata cache. - if (todr.Type is TypeReference tr && _cacheRef is not null) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); - if (resolved is not null) { return IsTypeBlittable(resolved); } - } - return false; - } - return false; - } - - /// - /// Resolves a to its - /// , handling both in-assembly (already a TypeDefinition) and - /// cross-assembly/TypeRef-row references via the metadata cache. Returns null when - /// the reference cannot be resolved. - /// - private static TypeDefinition? TryResolveStructTypeDef(AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tdr) - { - if (tdr.Type is TypeDefinition td) { return td; } - if (tdr.Type is TypeReference tr && _cacheRef is not null) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - return _cacheRef.Find(ns + "." + name); - } - return null; - } - - /// Mirrors C++ write_abi_enum. - public static void WriteAbiEnum(TypeWriter w, TypeDefinition type) - { - // The C++ version emits: write_struct_and_enum_marshaller_class, write_interface_entries_impl, - // write_struct_and_enum_com_wrappers_marshaller_attribute_impl, write_reference_impl. - // For now, emit a minimal marshaller class so the ComWrappersMarshaller attribute reference resolves. - string name = type.Name?.Value ?? string.Empty; - WriteStructEnumMarshallerClass(w, type); - WriteReferenceImpl(w, type); - - // In component mode, the C++ tool also emits the authoring metadata wrapper for enums. - if (w.Settings.Component) - { - WriteAuthoringMetadataType(w, type); - } - } - - /// Mirrors C++ write_abi_struct. - public static void WriteAbiStruct(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - - // Emit the underlying ABI struct only when not blittable AND not a mapped struct - // (mapped structs like Duration/KeyTime/RepeatBehavior have addition files that - // replace the public struct's field layout, so a per-field ABI struct can't be - // built directly from the projected type). - bool blittable = IsTypeBlittable(type); - string typeNs = type.Namespace?.Value ?? string.Empty; - string typeNm = type.Name?.Value ?? string.Empty; - bool isMappedStruct = MappedTypes.Get(typeNs, typeNm) is not null; - if (!blittable && !isMappedStruct) - { - // Mirror C++ write_abi_struct: in component mode emit metadata typename + mapped - // type attribute; otherwise emit the ComWrappers attribute. Both branches then - // emit [WindowsRuntimeClassName] + the struct definition with public ABI fields. - if (w.Settings.Component) - { - WriteWinRTMetadataTypeNameAttribute(w, type); - WriteWinRTMappedTypeAttribute(w, type); - } - else - { - WriteComWrapperMarshallerAttribute(w, type); - } - WriteValueTypeWinRTClassNameAttribute(w, type); - w.Write(Helpers.InternalAccessibility(w.Settings)); - w.Write(" unsafe struct "); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write("\n{\n"); - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - w.Write("public "); - // Truth uses void* for string and Nullable fields, the ABI struct for - // mapped value types (DateTime/TimeSpan), and the projected type for everything - // else (including enums and bool — their C# layout matches the WinRT ABI directly). - if (IsString(ft) || TryGetNullablePrimitiveMarshallerName(ft, out _)) - { - w.Write("void*"); - } - else if (IsMappedAbiValueType(ft)) - { - w.Write(GetMappedAbiTypeName(ft)); - } - else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tdr - && TryResolveStructTypeDef(tdr) is TypeDefinition fieldTd - && TypeCategorization.GetCategory(fieldTd) == TypeCategory.Struct - && !IsTypeBlittable(fieldTd)) - { - // Mirror C++ write_abi_type: non-blittable struct field uses ABI typedef name. - WriteTypedefName(w, fieldTd, TypedefNameType.ABI, false); - } - else - { - WriteProjectedSignature(w, ft, false); - } - w.Write(" "); - w.Write(field.Name?.Value ?? string.Empty); - w.Write(";\n"); - } - w.Write("}\n\n"); - } - else if (blittable && w.Settings.Component) - { - // For blittable component structs, the C++ tool emits the authoring metadata wrapper - // (a 'file static class T {}' with [WindowsRuntimeMetadataTypeName]/[WindowsRuntimeMappedType]/ - // [WindowsRuntimeReferenceType]/[ComWrappersMarshaller]/[WindowsRuntimeClassName]). - WriteAuthoringMetadataType(w, type); - } - - WriteStructEnumMarshallerClass(w, type); - WriteReferenceImpl(w, type); - } - - /// Mirrors C++ write_abi_delegate. - public static void WriteAbiDelegate(TypeWriter w, TypeDefinition type) - { - // Mirror the C++ tool's ordering exactly: - // write_delegate_marshaller - // write_delegate_vtbl - // write_native_delegate - // write_delegate_comwrappers_callback - // write_delegates_interface_entries_impl - // write_delegate_com_wrappers_marshaller_attribute_impl - // write_delegate_impl - // write_reference_impl - // (component) write_authoring_metadata_type - WriteDelegateMarshallerOnly(w, type); - WriteDelegateVftbl(w, type); - WriteNativeDelegate(w, type); - WriteDelegateComWrappersCallback(w, type); - WriteDelegateInterfaceEntriesImpl(w, type); - WriteDelegateComWrappersMarshallerAttribute(w, type); - WriteDelegateImpl(w, type); - WriteReferenceImpl(w, type); - - // In component mode, the C++ tool also emits the authoring metadata wrapper for delegates. - if (w.Settings.Component) - { - WriteAuthoringMetadataType(w, type); - } - } - - /// Emits the <DelegateName>Impl static class providing the CCW vtable for a delegate. - private static void WriteDelegateImpl(TypeWriter w, TypeDefinition type) - { - if (type.GenericParameters.Count > 0) { return; } - MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); - if (invoke is null) { return; } - MethodSig sig = new(invoke); - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); - - w.Write("\ninternal static unsafe class "); - w.Write(nameStripped); - w.Write("Impl\n{\n"); - w.Write(" [FixedAddressValueType]\n"); - w.Write(" private static readonly "); - w.Write(nameStripped); - w.Write("Vftbl Vftbl;\n\n"); - w.Write(" static "); - w.Write(nameStripped); - w.Write("Impl()\n {\n"); - w.Write(" *(IUnknownVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IUnknownVftbl*)IUnknownImpl.Vtable;\n"); - w.Write(" Vftbl.Invoke = &Invoke;\n"); - w.Write(" }\n\n"); - w.Write(" public static nint Vtable\n {\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => (nint)Unsafe.AsPointer(in Vftbl);\n }\n\n"); - - w.Write("[UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]\n"); - w.Write("private static int Invoke("); - WriteAbiParameterTypesPointer(w, sig, includeParamNames: true); - w.Write(")"); - - // Reuse the interface Do_Abi body emitter: delegates dispatch via __target.Invoke(...), - // which is exactly the same shape as interface CCW dispatch. Pass the delegate's - // projected name as 'ifaceFullName' and "Invoke" as 'methodName'. - string projectedDelegateForBody = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); - if (!projectedDelegateForBody.StartsWith("global::", System.StringComparison.Ordinal)) { projectedDelegateForBody = "global::" + projectedDelegateForBody; } - EmitDoAbiBodyIfSimple(w, sig, projectedDelegateForBody, "Invoke"); - w.Write("\n"); - - w.Write(" public static ref readonly Guid IID\n {\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => ref "); - w.Write(iidExpr); - w.Write(";\n }\n}\n"); - } - - - /// - /// Returns the interop assembly path for an array marshaller of a given element type. - /// The interop generator names array marshallers ABI.<typeNamespace>.<<assembly>ElementName>ArrayMarshaller - /// (typeNamespace prefix outside the brackets, and the element inside the brackets uses just the - /// type name without its namespace because depth=0 in the interop generator's AppendRawTypeName). - /// - private static string GetArrayMarshallerInteropPath(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature elementType, string encodedElement) - { - // The 'encodedElement' passed in uses the depth>0 form (assembly + hyphenated namespace + name), - // but inside the array brackets the interop generator uses the depth=0 form (assembly + just name). - // Re-encode the element with the top-level form for accurate matching. - string topLevelElement = EncodeArrayElementName(elementType); - // Resolve the element's namespace to determine the path prefix. - string ns = GetMappedNamespace(elementType); - if (string.IsNullOrEmpty(ns)) - { - return "ABI.<" + topLevelElement + ">ArrayMarshaller, WinRT.Interop"; - } - return "ABI." + ns + ".<" + topLevelElement + ">ArrayMarshaller, WinRT.Interop"; - } - - /// Returns the (possibly mapped) namespace of a type signature, or 'System' for fundamentals. - private static string GetMappedNamespace(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - // Fundamentals (string, bool, int, etc.) live in 'System' for ArrayMarshaller path purposes. - if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature) { return "System"; } - AsmResolver.DotNet.ITypeDefOrRef? td = null; - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tds) { td = tds.Type; } - else if (sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) { td = gi.GenericType; } - if (td is null) { return string.Empty; } - string typeNs = td.Namespace?.Value ?? string.Empty; - string typeName = td.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(typeNs, typeName); - return mapped is not null ? mapped.MappedNamespace : typeNs; - } - - /// - /// Encodes the array element type name as the interop generator's AppendRawTypeName at depth=0: - /// fundamentals use their short C# name; typedefs use just the type name (no namespace) prefixed - /// with the assembly marker; generic instances include their assembly marker, name, and type arguments. - /// - private static string EncodeArrayElementName(AsmResolver.DotNet.Signatures.TypeSignature elementType) - { - System.Text.StringBuilder sb = new(); - EncodeArrayElementNameInto(sb, elementType); - return sb.ToString(); - } - - private static void EncodeArrayElementNameInto(System.Text.StringBuilder sb, AsmResolver.DotNet.Signatures.TypeSignature sig) - { - // Special case for System.Guid: matches C++ guid_type handler in write_interop_dll_type_name. - // The depth=0 (top-level array element) form drops the namespace prefix and uses just the - // assembly marker + type name, so for Guid this becomes "<#corlib>Guid". - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature gtd - && gtd.Type?.Namespace?.Value == "System" - && gtd.Type?.Name?.Value == "Guid") - { - sb.Append("<#corlib>Guid"); - return; - } - switch (sig) - { - case AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib: - EncodeFundamental(sb, corlib, TypedefNameType.Projected); - return; - case AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td: - EncodeArrayElementForTypeDef(sb, td.Type, generic_args: null); - return; - case AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi: - EncodeArrayElementForTypeDef(sb, gi.GenericType, generic_args: gi.TypeArguments); - return; - default: - sb.Append(sig.FullName); - return; - } - } - - private static void EncodeArrayElementForTypeDef(System.Text.StringBuilder sb, AsmResolver.DotNet.ITypeDefOrRef type, System.Collections.Generic.IList? generic_args) - { - string typeNs = type.Namespace?.Value ?? string.Empty; - string typeName = type.Name?.Value ?? string.Empty; - // Apply mapped-type remapping (e.g. Windows.Foundation.IReference -> System.Nullable). - MappedType? mapped = MappedTypes.Get(typeNs, typeName); - if (mapped is not null) - { - typeNs = mapped.MappedNamespace; - typeName = mapped.MappedName; - } - // Replace generic arity backtick with apostrophe. - typeName = typeName.Replace('`', '\''); - - // Assembly marker prefix. Pass the type so that third-party (e.g. component-authored) - // types resolve to their actual assembly name (e.g. ) instead of - // defaulting to <#Windows>. - sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); - // Top-level: just the type name (no namespace). - sb.Append(typeName); - - // Generic arguments use the standard EncodeInteropTypeNameInto (depth > 0). - if (generic_args is { Count: > 0 }) - { - sb.Append('<'); - for (int i = 0; i < generic_args.Count; i++) - { - if (i > 0) { sb.Append('|'); } - EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); - } - sb.Append('>'); - } - } - - /// Mirrors C++ write_delegate_vtbl. - private static void WriteDelegateVftbl(TypeWriter w, TypeDefinition type) - { - if (type.GenericParameters.Count > 0) { return; } - MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); - if (invoke is null) { return; } - MethodSig sig = new(invoke); - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - w.Write("\n[StructLayout(LayoutKind.Sequential)]\n"); - w.Write("internal unsafe struct "); - w.Write(nameStripped); - w.Write("Vftbl\n{\n"); - w.Write(" public delegate* unmanaged[MemberFunction] QueryInterface;\n"); - w.Write(" public delegate* unmanaged[MemberFunction] AddRef;\n"); - w.Write(" public delegate* unmanaged[MemberFunction] Release;\n"); - w.Write(" public delegate* unmanaged[MemberFunction]<"); - WriteAbiParameterTypesPointer(w, sig); - w.Write(", int> Invoke;\n"); - w.Write("}\n"); - } - - /// Mirrors C++ write_native_delegate. - private static void WriteNativeDelegate(TypeWriter w, TypeDefinition type) - { - if (type.GenericParameters.Count > 0) { return; } - MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); - if (invoke is null) { return; } - MethodSig sig = new(invoke); - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - w.Write("\npublic static unsafe class "); - w.Write(nameStripped); - w.Write("NativeDelegate\n{\n"); - - w.Write(" public static unsafe "); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(nameStripped); - w.Write("Invoke(this WindowsRuntimeObjectReference thisReference"); - if (sig.Params.Count > 0) { w.Write(", "); } - WriteParameterList(w, sig); - w.Write(")"); - - // Reuse the interface caller body emitter. Delegate Invoke is at vtable slot 3 - // (after QI/AddRef/Release). Functionally equivalent to the truth's - // 'var abiInvoke = ((Vftbl*)*(void***)ThisPtr)->Invoke;' form, just routed - // through the slot-indexed dispatch shared with interface CCW callers. - EmitAbiMethodBodyIfSimple(w, sig, slot: 3, isNoExcept: Helpers.IsNoExcept(invoke)); - - w.Write("}\n"); - } - - /// Mirrors C++ write_delegates_interface_entries_impl. - private static void WriteDelegateInterfaceEntriesImpl(TypeWriter w, TypeDefinition type) - { - if (type.GenericParameters.Count > 0) { return; } - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); - string iidRefExpr = w.WriteTemp("%", new System.Action(_ => WriteIidReferenceExpression(w, type))); - - w.Write("\nfile static class "); - w.Write(nameStripped); - w.Write("InterfaceEntriesImpl\n{\n"); - w.Write(" [FixedAddressValueType]\n"); - w.Write(" public static readonly DelegateReferenceInterfaceEntries Entries;\n\n"); - w.Write(" static "); - w.Write(nameStripped); - w.Write("InterfaceEntriesImpl()\n {\n"); - w.Write(" Entries.Delegate.IID = "); - w.Write(iidExpr); - w.Write(";\n"); - w.Write(" Entries.Delegate.Vtable = "); - w.Write(nameStripped); - w.Write("Impl.Vtable;\n"); - w.Write(" Entries.DelegateReference.IID = "); - w.Write(iidRefExpr); - w.Write(";\n"); - w.Write(" Entries.DelegateReference.Vtable = "); - w.Write(nameStripped); - w.Write("ReferenceImpl.Vtable;\n"); - w.Write(" Entries.IPropertyValue.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IPropertyValue;\n"); - w.Write(" Entries.IPropertyValue.Vtable = global::WindowsRuntime.InteropServices.IPropertyValueImpl.OtherTypeVtable;\n"); - w.Write(" Entries.IStringable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable;\n"); - w.Write(" Entries.IStringable.Vtable = global::WindowsRuntime.InteropServices.IStringableImpl.Vtable;\n"); - w.Write(" Entries.IWeakReferenceSource.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IWeakReferenceSource;\n"); - w.Write(" Entries.IWeakReferenceSource.Vtable = global::WindowsRuntime.InteropServices.IWeakReferenceSourceImpl.Vtable;\n"); - w.Write(" Entries.IMarshal.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IMarshal;\n"); - w.Write(" Entries.IMarshal.Vtable = global::WindowsRuntime.InteropServices.IMarshalImpl.Vtable;\n"); - w.Write(" Entries.IAgileObject.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IAgileObject;\n"); - w.Write(" Entries.IAgileObject.Vtable = global::WindowsRuntime.InteropServices.IAgileObjectImpl.Vtable;\n"); - w.Write(" Entries.IInspectable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IInspectable;\n"); - w.Write(" Entries.IInspectable.Vtable = global::WindowsRuntime.InteropServices.IInspectableImpl.Vtable;\n"); - w.Write(" Entries.IUnknown.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IUnknown;\n"); - w.Write(" Entries.IUnknown.Vtable = global::WindowsRuntime.InteropServices.IUnknownImpl.Vtable;\n"); - w.Write(" }\n}\n"); - } - - /// Mirrors C++ write_temp_delegate_event_source_subclass. - public static void WriteTempDelegateEventSourceSubclass(TypeWriter w, TypeDefinition type) - { - // Skip generic delegates: only non-generic delegates get a per-delegate EventSource subclass. - // Generic delegates (e.g. EventHandler) use the generic EventHandlerEventSource directly. - if (type.GenericParameters.Count > 0) { return; } - - MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); - if (invoke is null) { return; } - MethodSig sig = new(invoke); - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - // Compute the projected type name (with global::) used as the generic argument. - string projectedName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); - if (!projectedName.StartsWith("global::", System.StringComparison.Ordinal)) - { - projectedName = "global::" + projectedName; - } - - w.Write("\npublic sealed unsafe class "); - w.Write(nameStripped); - w.Write("EventSource : EventSource<"); - w.Write(projectedName); - w.Write(">\n{\n"); - w.Write(" /// \n"); - w.Write(" public "); - w.Write(nameStripped); - w.Write("EventSource(WindowsRuntimeObjectReference nativeObjectReference, int index)\n : base(nativeObjectReference, index)\n {\n }\n\n"); - w.Write(" /// \n"); - w.Write(" protected override WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); - w.Write(projectedName); - w.Write(" value)\n {\n return "); - w.Write(nameStripped); - w.Write("Marshaller.ConvertToUnmanaged(value);\n }\n\n"); - w.Write(" /// \n"); - w.Write(" protected override EventSourceState<"); - w.Write(projectedName); - w.Write("> CreateEventSourceState()\n {\n return new EventState(GetNativeObjectReferenceThisPtrUnsafe(), Index);\n }\n\n"); - w.Write(" private sealed class EventState : EventSourceState<"); - w.Write(projectedName); - w.Write(">\n {\n"); - w.Write(" /// \n"); - w.Write(" public EventState(void* thisPtr, int index)\n : base(thisPtr, index)\n {\n }\n\n"); - w.Write(" /// \n"); - w.Write(" protected override "); - w.Write(projectedName); - w.Write(" GetEventInvoke()\n {\n"); - // Build parameter name list for the lambda. Lambda's parameter list MUST match the - // delegate's signature exactly, including in/out/ref modifiers - otherwise CS1676 fires - // when calling TargetDelegate.Invoke. Mirror C++ write_parmaeters. - w.Write(" return ("); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(", "); } - ParamCategory pc = ParamHelpers.GetParamCategory(sig.Params[i]); - if (pc == ParamCategory.Ref) { w.Write("in "); } - else if (pc == ParamCategory.Out || pc == ParamCategory.ReceiveArray) { w.Write("out "); } - string raw = sig.Params[i].Parameter.Name ?? "p"; - w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); - } - w.Write(") => TargetDelegate.Invoke("); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(", "); } - ParamCategory pc = ParamHelpers.GetParamCategory(sig.Params[i]); - if (pc == ParamCategory.Ref) { w.Write("in "); } - else if (pc == ParamCategory.Out || pc == ParamCategory.ReceiveArray) { w.Write("out "); } - string raw = sig.Params[i].Parameter.Name ?? "p"; - w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); - } - w.Write(");\n"); - w.Write(" }\n }\n}\n"); - } - - /// Mirrors C++ write_abi_class. - public static void WriteAbiClass(TypeWriter w, TypeDefinition type) - { - // Static classes don't get a *Marshaller (no instances). - if (TypeCategorization.IsStatic(type)) { return; } - // Mirror C++ write_abi_class: wrap class marshaller emission in #nullable enable/disable. - w.Write("#nullable enable\n"); - if (w.Settings.Component) - { - // Mirror C++ write_component_class_marshaller + write_authoring_metadata_type. - WriteComponentClassMarshaller(w, type); - WriteAuthoringMetadataType(w, type); - } - else - { - // Emit a ComWrappers marshaller class so the attribute reference resolves - WriteClassMarshallerStub(w, type); - } - w.Write("#nullable disable\n"); - } - - /// - /// Emits the simpler component-mode class marshaller. Mirrors C++ - /// write_component_class_marshaller. - /// - private static void WriteComponentClassMarshaller(TypeWriter w, TypeDefinition type) - { - string nameStripped = Helpers.StripBackticks(type.Name?.Value ?? string.Empty); - string typeNs = type.Namespace?.Value ?? string.Empty; - string projectedType = $"global::{typeNs}.{nameStripped}"; - - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); - - // Mirror C++ write_component_class_marshaller: if the default interface is a generic - // instantiation (e.g. IDictionary), emit an UnsafeAccessor extern declaration - // inside ConvertToUnmanaged that fetches the IID via WinRT.Interop's InterfaceIIDs class - // (since the IID for a generic instantiation is computed at runtime). The IID expression - // in the call then becomes '(null)' instead of a static InterfaceIIDs reference. - AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? defaultGenericInst = null; - if (defaultIface is AsmResolver.DotNet.TypeSpecification spec - && spec.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - defaultGenericInst = gi; - } - - string defaultIfaceIid; - if (defaultGenericInst is not null) - { - // Call the accessor: '>(null)'. - string accessorName = BuildIidPropertyNameForGenericInterface(w, defaultGenericInst); - defaultIfaceIid = accessorName + "(null)"; - } - else - { - defaultIfaceIid = defaultIface is not null - ? w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, defaultIface))) - : "default(global::System.Guid)"; - } - - w.Write("\npublic static unsafe class "); - w.Write(nameStripped); - w.Write("Marshaller\n{\n"); - w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); - w.Write(projectedType); - w.Write(" value)\n {\n"); - if (defaultGenericInst is not null) - { - // Emit the UnsafeAccessor declaration (uses 'object?' since component-mode - // marshallers run inside #nullable enable). - string accessorBlock = w.WriteTemp("%", new System.Action(_ => EmitUnsafeAccessorForIid(w, defaultGenericInst, isInNullableContext: true))); - // Re-emit each line indented by 8 spaces. - string[] accessorLines = accessorBlock.TrimEnd('\n').Split('\n'); - foreach (string accessorLine in accessorLines) - { - w.Write(" "); - w.Write(accessorLine); - w.Write("\n"); - } - } - w.Write(" return WindowsRuntimeInterfaceMarshaller<"); - w.Write(projectedType); - w.Write(">.ConvertToUnmanaged(value, "); - w.Write(defaultIfaceIid); - w.Write(");\n }\n\n"); - w.Write(" public static "); - w.Write(projectedType); - w.Write("? ConvertToManaged(void* value)\n {\n"); - w.Write(" return ("); - w.Write(projectedType); - w.Write("?) WindowsRuntimeObjectMarshaller.ConvertToManaged(value);\n }\n}\n"); - } - - /// - /// Emits the metadata wrapper type file static class <Name> {} with the conditional - /// set of attributes required for the type's category. Mirrors C++ - /// write_authoring_metadata_type. - /// - private static void WriteAuthoringMetadataType(TypeWriter w, TypeDefinition type) - { - string nameStripped = Helpers.StripBackticks(type.Name?.Value ?? string.Empty); - string typeNs = type.Namespace?.Value ?? string.Empty; - string projectedType = string.IsNullOrEmpty(typeNs) ? $"global::{nameStripped}" : $"global::{typeNs}.{nameStripped}"; - string fullName = string.IsNullOrEmpty(typeNs) ? nameStripped : $"{typeNs}.{nameStripped}"; - TypeCategory category = TypeCategorization.GetCategory(type); - - // [WindowsRuntimeReferenceType(typeof(?))] for non-delegate, non-class types - // (i.e. enums, structs, interfaces). - if (category != TypeCategory.Delegate && category != TypeCategory.Class) - { - w.Write("[WindowsRuntimeReferenceType(typeof("); - w.Write(projectedType); - w.Write("?))]\n"); - } - - // [ABI..ComWrappersMarshaller] for non-struct, non-class types - // (delegates, enums, interfaces). - if (category != TypeCategory.Struct && category != TypeCategory.Class) - { - w.Write("[ABI."); - w.Write(typeNs); - w.Write("."); - w.Write(nameStripped); - w.Write("ComWrappersMarshaller]\n"); - } - - // [WindowsRuntimeClassName("Windows.Foundation.IReference`1<.>")] for non-class types. - if (category != TypeCategory.Class) - { - w.Write("[WindowsRuntimeClassName(\"Windows.Foundation.IReference`1<"); - w.Write(fullName); - w.Write(">\")]\n"); - } - - w.Write("[WindowsRuntimeMetadataTypeName(\""); - w.Write(fullName); - w.Write("\")]\n"); - w.Write("[WindowsRuntimeMappedType(typeof("); - w.Write(projectedType); - w.Write("))]\n"); - w.Write("file static class "); - w.Write(nameStripped); - w.Write(" {}\n"); - } - - /// Mirrors C++ write_abi_interface. - public static void WriteAbiInterface(TypeWriter w, TypeDefinition type) - { - // Generic interfaces are handled by interopgen - if (type.GenericParameters.Count > 0) { return; } - - // The C++ also emits write_static_abi_classes here - we emit a basic stub for now - WriteInterfaceMarshallerStub(w, type); - - // For internal projections, just the static ABI methods class is enough. - if (TypeCategorization.IsProjectionInternal(type)) { return; } - - WriteInterfaceVftbl(w, type); - WriteInterfaceImpl(w, type); - WriteInterfaceIdicImpl(w, type); - WriteInterfaceMarshaller(w, type); - } - - /// Mirrors C++ emit_impl_type. - public static bool EmitImplType(TypeWriter w, TypeDefinition type) - { - if (w.Settings.Component) { return true; } - if (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) - { - // Mirror C++ emit_impl_type: only emit Impl for exclusive-to interfaces if at least - // one interface impl on the exclusive_to class is marked [Overridable] and matches - // this interface. Otherwise the Impl wouldn't be reachable as a CCW. - TypeDefinition? exclusiveToType = GetExclusiveToType(type); - if (exclusiveToType is null) { return true; } - bool hasOverridable = false; - foreach (InterfaceImplementation impl in exclusiveToType.Interfaces) - { - if (impl.Interface is null) { continue; } - TypeDefinition? ifaceTd = ResolveInterfaceTypeDef(impl.Interface); - if (ifaceTd == type && Helpers.IsOverridable(impl)) { hasOverridable = true; break; } - } - return hasOverridable; - } - return true; - } - - /// - /// Returns the parent class for an interface marked [ExclusiveToAttribute(typeof(T))]. - /// Mirrors C++ get_exclusive_to_type. - /// - internal static TypeDefinition? GetExclusiveToType(TypeDefinition iface) - { - if (_cacheRef is null) { return null; } - for (int i = 0; i < iface.CustomAttributes.Count; i++) - { - CustomAttribute attr = iface.CustomAttributes[i]; - ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; - if (attrType is null) { continue; } - if (attrType.Namespace?.Value != "Windows.Foundation.Metadata" || - attrType.Name?.Value != "ExclusiveToAttribute") { continue; } - if (attr.Signature is null) { continue; } - for (int j = 0; j < attr.Signature.FixedArguments.Count; j++) - { - AsmResolver.DotNet.Signatures.CustomAttributeArgument arg = attr.Signature.FixedArguments[j]; - if (arg.Element is AsmResolver.DotNet.Signatures.TypeSignature sig) - { - string fullName = sig.FullName ?? string.Empty; - TypeDefinition? td = _cacheRef.Find(fullName); - if (td is not null) { return td; } - } - else if (arg.Element is string s) - { - TypeDefinition? td = _cacheRef.Find(s); - if (td is not null) { return td; } - } - } - } - return null; - } - - /// Resolves an InterfaceImpl's interface reference to a TypeDefinition (same module or via metadata cache). - private static TypeDefinition? ResolveInterfaceTypeDef(ITypeDefOrRef ifaceRef) - { - if (ifaceRef is TypeDefinition td) { return td; } - if (ifaceRef is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - ITypeDefOrRef? gen = gi.GenericType; - if (gen is TypeDefinition gtd) { return gtd; } - if (gen is TypeReference gtr && _cacheRef is not null) - { - string ns = gtr.Namespace?.Value ?? string.Empty; - string nm = gtr.Name?.Value ?? string.Empty; - return _cacheRef.Find(ns + "." + nm); - } - } - if (ifaceRef is TypeReference tr && _cacheRef is not null) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string nm = tr.Name?.Value ?? string.Empty; - return _cacheRef.Find(ns + "." + nm); - } - return null; - } - - /// Mirrors C++ get_vmethod_name. - public static string GetVMethodName(TypeDefinition type, MethodDefinition method) - { - // Index of method in the type's method list - int index = 0; - foreach (MethodDefinition m in type.Methods) - { - if (m == method) { break; } - index++; - } - return (method.Name?.Value ?? string.Empty) + "_" + index.ToString(System.Globalization.CultureInfo.InvariantCulture); - } - - /// Mirrors C++ write_abi_parameter_types_pointer. - public static void WriteAbiParameterTypesPointer(TypeWriter w, MethodSig sig) - { - WriteAbiParameterTypesPointer(w, sig, includeParamNames: false); - } - - /// - /// Writes the ABI parameter types for a vtable function pointer signature, optionally - /// including parameter names (for method declarations vs. function pointer type lists). - /// - public static void WriteAbiParameterTypesPointer(TypeWriter w, MethodSig sig, bool includeParamNames) - { - // void* thisPtr, then each param's ABI type, then return type pointer - w.Write("void*"); - if (includeParamNames) { w.Write(" thisPtr"); } - for (int i = 0; i < sig.Params.Count; i++) - { - w.Write(", "); - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (p.Type is AsmResolver.DotNet.Signatures.SzArrayTypeSignature sz) - { - // length pointer + value pointer. Mirrors C++ write_abi_signature for SzArray - // input params (code_writers.h:1305) which always emits "uint __%Size, void* %" - // regardless of element type. - if (includeParamNames) - { - w.Write("uint "); - w.Write("__"); - w.Write(p.Parameter.Name ?? "param"); - w.Write("Size, void* "); - Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); - } - else - { - w.Write("uint, void*"); - } - _ = sz; - } - else if (p.Type is AsmResolver.DotNet.Signatures.ByReferenceTypeSignature br) - { - // Special case: 'out T[]' is a ReceiveArray ABI signature: (uint* size, T** data). - if (br.BaseType is AsmResolver.DotNet.Signatures.SzArrayTypeSignature brSz && cat == ParamCategory.ReceiveArray) - { - bool isRefElemBr = IsString(brSz.BaseType) || IsRuntimeClassOrInterface(brSz.BaseType) || IsObject(brSz.BaseType) || IsGenericInstance(brSz.BaseType); - if (includeParamNames) - { - w.Write("uint* __"); - w.Write(p.Parameter.Name ?? "param"); - w.Write("Size, "); - if (isRefElemBr) { w.Write("void*** "); } - else - { - WriteAbiType(w, TypeSemanticsFactory.Get(brSz.BaseType)); - w.Write("** "); - } - Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); - } - else - { - w.Write("uint*, "); - if (isRefElemBr) { w.Write("void***"); } - else - { - WriteAbiType(w, TypeSemanticsFactory.Get(brSz.BaseType)); - w.Write("**"); - } - } - } - else - { - WriteAbiType(w, TypeSemanticsFactory.Get(br.BaseType)); - w.Write("*"); - if (includeParamNames) - { - w.Write(" "); - Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); - } - } - } - else - { - WriteAbiType(w, TypeSemanticsFactory.Get(p.Type)); - if (cat is ParamCategory.Out or ParamCategory.Ref) { w.Write("*"); } - if (includeParamNames) - { - w.Write(" "); - Helpers.WriteEscapedIdentifier(w, p.Parameter.Name ?? "param"); - } - } - } - // Return parameter - if (sig.ReturnType is not null) - { - w.Write(", "); - string retName = GetReturnParamName(sig); - string retSizeName = GetReturnSizeParamName(sig); - // Special handling for SzArray return types: WinRT projects them as a (uint*, T**) pair. - if (sig.ReturnType is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz) - { - if (includeParamNames) - { - w.Write("uint* "); - w.Write(retSizeName); - w.Write(", "); - WriteAbiType(w, TypeSemanticsFactory.Get(retSz.BaseType)); - w.Write("** "); - w.Write(retName); - } - else - { - w.Write("uint*, "); - WriteAbiType(w, TypeSemanticsFactory.Get(retSz.BaseType)); - w.Write("**"); - } - } - else - { - WriteAbiType(w, TypeSemanticsFactory.Get(sig.ReturnType)); - w.Write("*"); - if (includeParamNames) { w.Write(' '); w.Write(retName); } - } - } - } - - /// - /// Returns the metadata-derived name for the return parameter, or the C++ default __return_value__. - /// Mirrors method_signature::return_param_name() in helpers.h. - /// - internal static string GetReturnParamName(MethodSig sig) - { - string? n = sig.ReturnParam?.Name?.Value; - if (string.IsNullOrEmpty(n)) { return "__return_value__"; } - return Helpers.IsKeyword(n) ? "@" + n : n; - } - - /// - /// Returns the local-variable name for the return parameter on the server side. Mirrors C++ - /// abi_marshaler::get_marshaler_local() which prefixes __ to the param name. - /// - internal static string GetReturnLocalName(MethodSig sig) - { - return "__" + GetReturnParamName(sig); - } - - /// Returns '__<returnName>Size' (matches C++ '__%Size' convention) — by default '____return_value__Size' for the standard '__return_value__' return param. - internal static string GetReturnSizeParamName(MethodSig sig) - { - // Mirrors C++ 'write_abi_parameter_types_pointer' which writes '__%Size' over the return param name. - return "__" + GetReturnParamName(sig) + "Size"; - } - - /// Mirrors C++ write_interface_vftbl. - public static void WriteInterfaceVftbl(TypeWriter w, TypeDefinition type) - { - if (!EmitImplType(w, type)) { return; } - if (type.GenericParameters.Count > 0) { return; } - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - w.Write("\n[StructLayout(LayoutKind.Sequential)]\n"); - w.Write("internal unsafe struct "); - w.Write(nameStripped); - w.Write("Vftbl\n{\n"); - w.Write("public delegate* unmanaged[MemberFunction] QueryInterface;\n"); - w.Write("public delegate* unmanaged[MemberFunction] AddRef;\n"); - w.Write("public delegate* unmanaged[MemberFunction] Release;\n"); - w.Write("public delegate* unmanaged[MemberFunction] GetIids;\n"); - w.Write("public delegate* unmanaged[MemberFunction] GetRuntimeClassName;\n"); - w.Write("public delegate* unmanaged[MemberFunction] GetTrustLevel;\n"); - - foreach (MethodDefinition method in type.Methods) - { - string vm = GetVMethodName(type, method); - MethodSig sig = new(method); - w.Write("public delegate* unmanaged[MemberFunction]<"); - WriteAbiParameterTypesPointer(w, sig); - w.Write(", int> "); - w.Write(vm); - w.Write(";\n"); - } - w.Write("}\n"); - } - - /// Mirrors C++ write_interface_impl (simplified). - public static void WriteInterfaceImpl(TypeWriter w, TypeDefinition type) - { - if (!EmitImplType(w, type)) { return; } - if (type.GenericParameters.Count > 0) { return; } - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - w.Write("\npublic static unsafe class "); - w.Write(nameStripped); - w.Write("Impl\n{\n"); - w.Write("[FixedAddressValueType]\n"); - w.Write("private static readonly "); - w.Write(nameStripped); - w.Write("Vftbl Vftbl;\n\n"); - - w.Write("static "); - w.Write(nameStripped); - w.Write("Impl()\n{\n"); - w.Write(" *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable;\n"); - foreach (MethodDefinition method in type.Methods) - { - string vm = GetVMethodName(type, method); - w.Write(" Vftbl."); - w.Write(vm); - w.Write(" = &Do_Abi_"); - w.Write(vm); - w.Write(";\n"); - } - w.Write("}\n\n"); - - w.Write("public static ref readonly Guid IID\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => ref "); - WriteIidGuidReference(w, type); - w.Write(";\n}\n\n"); - - w.Write("public static nint Vtable\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => (nint)Unsafe.AsPointer(in Vftbl);\n}\n\n"); - - // Do_Abi_* implementations: emit real bodies for simple primitive cases, - // throw null! for everything else (deferred — needs full per-parameter marshalling). - // Mirror C++: in component mode, exclusive-to interfaces dispatch to the OWNING class - // type (not the interface) since the authored class IS the implementation. This is what - // 'write_method_abi_invoke' produces because 'method.Parent()' is treated through - // 'does_abi_interface_implement_ccw_interface' for authoring scenarios. - // - // EXCEPTION: static factory interfaces ([Static] attr on the class) and activation - // factory interfaces ([Activatable(typeof(IFooFactory))]) are implemented by the - // generated 'ABI.Impl..'/' types, NOT by the user runtime - // class. For those, the dispatch target must be 'global::ABI.Impl..'. - TypeDefinition? exclusiveToOwner = null; - bool exclusiveIsFactoryOrStatic = false; - if (w.Settings.Component) - { - MetadataCache? cache = GetMetadataCache(); - if (cache is not null) - { - exclusiveToOwner = Helpers.GetExclusiveToType(type, cache); - if (exclusiveToOwner is not null) - { - foreach (KeyValuePair kv in AttributedTypes.Get(exclusiveToOwner, cache)) - { - if (kv.Value.Type == type && (kv.Value.Statics || kv.Value.Activatable)) - { - exclusiveIsFactoryOrStatic = true; - break; - } - } - } - } - } - - string ifaceFullName; - if (exclusiveToOwner is not null && !exclusiveIsFactoryOrStatic) - { - string ownerNs = exclusiveToOwner.Namespace?.Value ?? string.Empty; - string ownerNm = Helpers.StripBackticks(exclusiveToOwner.Name?.Value ?? string.Empty); - ifaceFullName = string.IsNullOrEmpty(ownerNs) - ? "global::" + ownerNm - : "global::" + ownerNs + "." + ownerNm; - } - else if (exclusiveToOwner is not null && exclusiveIsFactoryOrStatic) - { - // Factory/static interfaces in authoring mode are implemented by the generated - // 'global::ABI.Impl..' type that the activation factory CCW exposes. - string ifaceNs = type.Namespace?.Value ?? string.Empty; - string ifaceNm = Helpers.StripBackticks(type.Name?.Value ?? string.Empty); - ifaceFullName = string.IsNullOrEmpty(ifaceNs) - ? "global::ABI.Impl." + ifaceNm - : "global::ABI.Impl." + ifaceNs + "." + ifaceNm; - } - else - { - ifaceFullName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); - if (!ifaceFullName.StartsWith("global::", System.StringComparison.Ordinal)) { ifaceFullName = "global::" + ifaceFullName; } - } - - // Build a map of event add/remove methods to their event so we can emit the table field - // and the proper Do_Abi_add_*/Do_Abi_remove_* bodies (mirrors C++ write_event_abi_invoke). - System.Collections.Generic.Dictionary? eventMap = BuildEventMethodMap(type); - - // Build sets of property accessors and event accessors so the first loop below can - // iterate "regular" methods (non-property, non-event) only. C++ emits Do_Abi bodies in - // this order: methods first, then properties (setter before getter per write_property_abi_invoke - // at code_writers.h:8245), then events. Mine previously emitted them in pure metadata - // (slot) order which matched neither truth nor C++. - System.Collections.Generic.HashSet propertyAccessors = new(); - foreach (PropertyDefinition prop in type.Properties) - { - if (prop.GetMethod is MethodDefinition g) { propertyAccessors.Add(g); } - if (prop.SetMethod is MethodDefinition s) { propertyAccessors.Add(s); } - } - - // Local helper to emit a single Do_Abi method body for a given MethodDefinition. - void EmitOneDoAbi(MethodDefinition method) - { - string vm = GetVMethodName(type, method); - MethodSig sig = new(method); - string mname = method.Name?.Value ?? string.Empty; - - // If this method is an event add accessor, emit the per-event ConditionalWeakTable - // before the Do_Abi method (mirrors C++ ordering). - if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) - { - EmitEventTableField(w, evt, type, ifaceFullName); - } - - w.Write("[UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]\n"); - w.Write("private static unsafe int Do_Abi_"); - w.Write(vm); - w.Write("("); - WriteAbiParameterTypesPointer(w, sig, includeParamNames: true); - w.Write(")"); - - if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt2)) - { - if (evt2.AddMethod == method) - { - EmitDoAbiAddEvent(w, evt2, sig, ifaceFullName); - } - else - { - EmitDoAbiRemoveEvent(w, evt2, sig, ifaceFullName); - } - } - else - { - EmitDoAbiBodyIfSimple(w, sig, ifaceFullName, mname); - } - } - - // 1. Regular methods (non-property, non-event), in metadata order. - foreach (MethodDefinition method in type.Methods) - { - if (propertyAccessors.Contains(method)) { continue; } - if (eventMap is not null && eventMap.ContainsKey(method)) { continue; } - EmitOneDoAbi(method); - } - - // 2. Properties, in metadata order. Setter before getter per write_property_abi_invoke. - foreach (PropertyDefinition prop in type.Properties) - { - if (prop.SetMethod is MethodDefinition s) { EmitOneDoAbi(s); } - if (prop.GetMethod is MethodDefinition g) { EmitOneDoAbi(g); } - } - - // 3. Events, in metadata order. Add then Remove (matches metadata order from BuildEventMethodMap). - foreach (EventDefinition evt in type.Events) - { - if (evt.AddMethod is MethodDefinition a) { EmitOneDoAbi(a); } - if (evt.RemoveMethod is MethodDefinition r) { EmitOneDoAbi(r); } - } - w.Write("}\n"); - } - - /// Build a method-to-event map for add/remove accessors of a type. - private static System.Collections.Generic.Dictionary? BuildEventMethodMap(TypeDefinition type) - { - if (type.Events.Count == 0) { return null; } - System.Collections.Generic.Dictionary map = new(); - foreach (EventDefinition evt in type.Events) - { - if (evt.AddMethod is MethodDefinition add) { map[add] = evt; } - if (evt.RemoveMethod is MethodDefinition rem) { map[rem] = evt; } - } - return map; - } - - /// - /// Emits the per-event ConditionalWeakTable<TInterface, EventRegistrationTokenTable<THandler>> - /// backing field property. Mirrors the table emission in C++ write_event_abi_invoke. - /// The is the dispatch target type for the CCW (computed by - /// the caller in EmitDoAbiBodyIfSimple) — for instance events on authored classes this is - /// the runtime class type, NOT the ABI.Impl interface. - /// - private static void EmitEventTableField(TypeWriter w, EventDefinition evt, TypeDefinition iface, string ifaceFullName) - { - string evName = evt.Name?.Value ?? "Event"; - string evtType = w.WriteTemp("%", new System.Action(_ => WriteEventType(w, evt))); - - w.Write("\nprivate static ConditionalWeakTable<"); - w.Write(ifaceFullName); - w.Write(", EventRegistrationTokenTable<"); - w.Write(evtType); - w.Write(">> _"); - w.Write(evName); - w.Write("\n{\n"); - w.Write(" [MethodImpl(MethodImplOptions.AggressiveInlining)]\n"); - w.Write(" get\n {\n"); - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" static ConditionalWeakTable<"); - w.Write(ifaceFullName); - w.Write(", EventRegistrationTokenTable<"); - w.Write(evtType); - w.Write(">> MakeTable()\n {\n"); - w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(ref field, [], null);\n\n"); - w.Write(" return global::System.Threading.Volatile.Read(in field);\n"); - w.Write(" }\n\n"); - w.Write(" return global::System.Threading.Volatile.Read(in field) ?? MakeTable();\n }\n}\n"); - } - - /// - /// Emits the body of the Do_Abi_add_<EventName>_N method. Mirrors the corresponding - /// branch in C++ write_event_abi_invoke. - /// - private static void EmitDoAbiAddEvent(TypeWriter w, EventDefinition evt, MethodSig sig, string ifaceFullName) - { - string evName = evt.Name?.Value ?? "Event"; - // Handler is the (last) input parameter of the add method. The emitted parameter name in the - // signature comes from WriteAbiParameterTypesPointer which uses the metadata name verbatim. - string handlerRawName = sig.Params.Count > 0 ? (sig.Params[^1].Parameter.Name ?? "handler") : "handler"; - string handlerRef = Helpers.IsKeyword(handlerRawName) ? "@" + handlerRawName : handlerRawName; - - // The cookie/token return parameter takes the metadata return param name (matches truth). - string cookieName = GetReturnParamName(sig); - - AsmResolver.DotNet.Signatures.TypeSignature evtTypeSig = evt.EventType!.ToTypeSignature(false); - bool isGeneric = evtTypeSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; - - w.Write("\n{\n"); - w.Write(" *"); - w.Write(cookieName); - w.Write(" = default;\n"); - w.Write(" try\n {\n"); - w.Write(" var __this = ComInterfaceDispatch.GetInstance<"); - w.Write(ifaceFullName); - w.Write(">((ComInterfaceDispatch*)thisPtr);\n"); - - if (isGeneric) - { - string interopTypeName = EncodeInteropTypeName(evtTypeSig, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, evtTypeSig, false))); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); - w.Write(" static extern "); - w.Write(projectedTypeName); - w.Write(" ConvertToManaged([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, void* value);\n"); - w.Write(" var __handler = ConvertToManaged(null, "); - w.Write(handlerRef); - w.Write(");\n"); - } - else - { - w.Write(" var __handler = "); - WriteTypeName(w, TypeSemanticsFactory.Get(evtTypeSig), TypedefNameType.ABI, false); - w.Write("Marshaller.ConvertToManaged("); - w.Write(handlerRef); - w.Write(");\n"); - } - - w.Write(" *"); - w.Write(cookieName); - w.Write(" = _"); - w.Write(evName); - w.Write(".GetOrCreateValue(__this).AddEventHandler(__handler);\n"); - w.Write(" __this."); - w.Write(evName); - w.Write(" += __handler;\n"); - w.Write(" return 0;\n }\n"); - w.Write(" catch (Exception __exception__)\n {\n"); - w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__);\n }\n}\n"); - } - - /// - /// Emits the body of the Do_Abi_remove_<EventName>_N method. Mirrors the corresponding - /// branch in C++ write_event_abi_invoke. - /// - private static void EmitDoAbiRemoveEvent(TypeWriter w, EventDefinition evt, MethodSig sig, string ifaceFullName) - { - string evName = evt.Name?.Value ?? "Event"; - string tokenRawName = sig.Params.Count > 0 ? (sig.Params[^1].Parameter.Name ?? "token") : "token"; - string tokenRef = Helpers.IsKeyword(tokenRawName) ? "@" + tokenRawName : tokenRawName; - - w.Write("\n{\n"); - w.Write(" try\n {\n"); - w.Write(" var __this = ComInterfaceDispatch.GetInstance<"); - w.Write(ifaceFullName); - w.Write(">((ComInterfaceDispatch*)thisPtr);\n"); - w.Write(" if(__this is not null && _"); - w.Write(evName); - w.Write(".TryGetValue(__this, out var __table) && __table.RemoveEventHandler("); - w.Write(tokenRef); - w.Write(", out var __handler))\n {\n"); - w.Write(" __this."); - w.Write(evName); - w.Write(" -= __handler;\n"); - w.Write(" }\n"); - w.Write(" return 0;\n }\n"); - w.Write(" catch (Exception __exception__)\n {\n"); - w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__);\n }\n}\n"); - } - - /// - /// Emits a real Do_Abi (CCW) body for the cases we can handle. Mirrors C++ - /// write_abi_method_call_marshalers (code_writers.h:6682) which - /// unconditionally emits a real body via the abi_marshaler abstraction - /// for every WinRT-valid signature. - /// - private static void EmitDoAbiBodyIfSimple(TypeWriter w, MethodSig sig, string ifaceFullName, string methodName) - { - AsmResolver.DotNet.Signatures.TypeSignature? rt = sig.ReturnType; - - // String params drive whether we need HString header allocation in the body. - bool hasStringParams = false; - foreach (ParamInfo p in sig.Params) - { - if (IsString(p.Type)) { hasStringParams = true; break; } - } - bool returnIsReceiveArrayDoAbi = rt is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSzAbi - && (IsBlittablePrimitive(retSzAbi.BaseType) || IsAnyStruct(retSzAbi.BaseType) - || IsString(retSzAbi.BaseType) || IsRuntimeClassOrInterface(retSzAbi.BaseType) || IsObject(retSzAbi.BaseType) - || IsComplexStruct(retSzAbi.BaseType)); - bool returnIsHResultExceptionDoAbi = rt is not null && IsHResultException(rt); - bool returnIsString = rt is not null && IsString(rt); - bool returnIsRefType = rt is not null && (IsRuntimeClassOrInterface(rt) || IsObject(rt) || IsGenericInstance(rt)); - bool returnIsGenericInstance = rt is not null && IsGenericInstance(rt); - bool returnIsBlittableStruct = rt is not null && IsAnyStruct(rt); - - bool isGetter = methodName.StartsWith("get_", System.StringComparison.Ordinal); - bool isSetter = methodName.StartsWith("put_", System.StringComparison.Ordinal); - bool isAddEvent = methodName.StartsWith("add_", System.StringComparison.Ordinal); - bool isRemoveEvent = methodName.StartsWith("remove_", System.StringComparison.Ordinal); - - if (isAddEvent || isRemoveEvent) - { - // Events go through dedicated EmitDoAbiAddEvent / EmitDoAbiRemoveEvent paths - // upstream (see lines 1153-1159). If we reach here for an event accessor it's a - // generator bug. Defensive guard against future regressions. - throw new System.InvalidOperationException( - $"EmitDoAbiBodyIfSimple: unexpectedly called for event accessor '{methodName}' " + - $"on '{ifaceFullName}'. Events should dispatch through EmitDoAbiAddEvent / EmitDoAbiRemoveEvent."); - } - - w.Write("\n{\n"); - string retParamName = GetReturnParamName(sig); - string retSizeParamName = GetReturnSizeParamName(sig); - // The local name for the unmarshalled return value mirrors C++ - // 'abi_marshaler::get_marshaler_local()' which prefixes '__' to the param name. - // For the default '__return_value__' param this becomes '____return_value__'. - string retLocalName = "__" + retParamName; - - // Mirror C++ ordering: emit any [UnsafeAccessor] static local function declarations - // at the TOP of the method body (before local declarations and the try block). The - // actual call sites later in the body just reference the already-declared accessor. - // For a generic-instance return type, the accessor is named ConvertToUnmanaged_. - // Skip Nullable returns: those use Marshaller.BoxToUnmanaged at the call site - // instead of the generic-instance UnsafeAccessor (V3-M7). - if (returnIsGenericInstance && !(rt is not null && IsNullableT(rt))) - { - string interopTypeName = EncodeInteropTypeName(rt!, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt!, false))); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); - w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); - w.Write(retParamName); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, "); - w.Write(projectedTypeName); - w.Write(" value);\n\n"); - } - - // Hoist [UnsafeAccessor] declarations for Out generic-instance params: - // ConvertToUnmanaged_ wraps the projected value into a WindowsRuntimeObjectReferenceValue. - // The body's writeback later references these already-declared accessors. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); - if (!IsGenericInstance(uOut)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string interopTypeName = EncodeInteropTypeName(uOut, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, uOut, false))); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); - w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, "); - w.Write(projectedTypeName); - w.Write(" value);\n\n"); - } - - // Mirror C++ ordering: hoist [UnsafeAccessor] declarations for ReceiveArray (out T[]) - // ConvertToUnmanaged_ and the return-array ConvertToUnmanaged_ to the - // top of the method body, before locals and the try block. The actual call sites later - // in the body reference these already-declared accessors. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.ReceiveArray) { continue; } - string raw = p.Parameter.Name ?? "param"; - AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sza.BaseType)))); - string elementInteropArg = EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); - string marshallerPath = GetArrayMarshallerInteropPath(w, sza.BaseType, elementInteropArg); - string elementAbi = IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType) - ? "void*" - : IsComplexStruct(sza.BaseType) - ? GetAbiStructTypeName(w, sza.BaseType) - : IsAnyStruct(sza.BaseType) - ? GetBlittableStructAbiType(w, sza.BaseType) - : GetAbiPrimitiveType(sza.BaseType); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); - w.Write(" static extern void ConvertToUnmanaged_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(marshallerPath); - w.Write("\")] object _, ReadOnlySpan<"); - w.Write(elementProjected); - w.Write("> span, out uint length, out "); - w.Write(elementAbi); - w.Write("* data);\n\n"); - } - if (returnIsReceiveArrayDoAbi && rt is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSzHoist) - { - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(retSzHoist.BaseType)))); - string elementAbi = IsString(retSzHoist.BaseType) || IsRuntimeClassOrInterface(retSzHoist.BaseType) || IsObject(retSzHoist.BaseType) - ? "void*" - : IsComplexStruct(retSzHoist.BaseType) - ? GetAbiStructTypeName(w, retSzHoist.BaseType) - : IsAnyStruct(retSzHoist.BaseType) - ? GetBlittableStructAbiType(w, retSzHoist.BaseType) - : GetAbiPrimitiveType(retSzHoist.BaseType); - string elementInteropArg = EncodeInteropTypeName(retSzHoist.BaseType, TypedefNameType.Projected); - string marshallerPath = GetArrayMarshallerInteropPath(w, retSzHoist.BaseType, elementInteropArg); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); - w.Write(" static extern void ConvertToUnmanaged_"); - w.Write(retParamName); - w.Write("([UnsafeAccessorType(\""); - w.Write(marshallerPath); - w.Write("\")] object _, ReadOnlySpan<"); - w.Write(elementProjected); - w.Write("> span, out uint length, out "); - w.Write(elementAbi); - w.Write("* data);\n\n"); - } - - // Mirror C++ ordering: declare the return local first with default value, then zero - // the OUT pointer(s). The actual assignment happens inside the try block. - if (rt is not null) - { - if (returnIsString) - { - w.Write(" string "); - w.Write(retLocalName); - w.Write(" = default;\n"); - } - else if (returnIsRefType) - { - string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); - w.Write(" "); - w.Write(projected); - w.Write(" "); - w.Write(retLocalName); - w.Write(" = default;\n"); - } - else if (returnIsReceiveArrayDoAbi) - { - string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); - w.Write(" "); - w.Write(projected); - w.Write(" "); - w.Write(retLocalName); - w.Write(" = default;\n"); - } - else - { - string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); - w.Write(" "); - w.Write(projected); - w.Write(" "); - w.Write(retLocalName); - w.Write(" = default;\n"); - } - } - - if (rt is not null) - { - if (returnIsReceiveArrayDoAbi) - { - w.Write(" *"); - w.Write(retParamName); - w.Write(" = default;\n"); - w.Write(" *"); - w.Write(retSizeParamName); - w.Write(" = default;\n"); - } - else - { - w.Write(" *"); - w.Write(retParamName); - w.Write(" = default;\n"); - } - } - // For each out parameter, clear the destination and declare a local. - // NOTE: Ref params (WinRT 'in T' / 'ref const T') are READ-ONLY inputs from the caller's - // perspective. Do NOT zero * (it's the input value) and do NOT declare a local - // (we read directly via *). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(" *"); - w.Write(ptr); - w.Write(" = default;\n"); - } - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - string raw = p.Parameter.Name ?? "param"; - // Use the projected (non-ABI) type for the local variable. - // Strip ByRef and CustomModifier wrappers to get the underlying base type. - AsmResolver.DotNet.Signatures.TypeSignature underlying = StripByRefAndCustomModifiers(p.Type); - string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, underlying, false))); - w.Write(" "); - w.Write(projected); - w.Write(" __"); - w.Write(raw); - w.Write(" = default;\n"); - } - // For each ReceiveArray parameter (out T[]), zero the destination + size out pointers - // and declare a managed array local. The managed call passes 'out __' and after - // the call we copy to the ABI buffer via UnsafeAccessor. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.ReceiveArray) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sza.BaseType)))); - w.Write(" *"); - w.Write(ptr); - w.Write(" = default;\n"); - w.Write(" *__"); - w.Write(raw); - w.Write("Size = default;\n"); - w.Write(" "); - w.Write(elementProjected); - w.Write("[] __"); - w.Write(raw); - w.Write(" = default;\n"); - } - // For each blittable array (PassArray / FillArray) parameter, declare a Span local that - // wraps the (length, pointer) pair from the ABI signature. - // For non-blittable element types (string/runtime class/object), declare InlineArray16 + - // ArrayPool fallback then CopyToManaged via UnsafeAccessor. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature sz) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)))); - bool isBlittableElem = IsBlittablePrimitive(sz.BaseType) || IsAnyStruct(sz.BaseType); - if (isBlittableElem) - { - w.Write(" "); - w.Write(cat == ParamCategory.PassArray ? "ReadOnlySpan<" : "Span<"); - w.Write(elementProjected); - w.Write("> __"); - w.Write(raw); - w.Write(" = new("); - w.Write(ptr); - w.Write(", (int)__"); - w.Write(raw); - w.Write("Size);\n"); - } - else - { - // Non-blittable element: InlineArray16 + ArrayPool with size from ABI. - w.Write("\n Unsafe.SkipInit(out InlineArray16<"); - w.Write(elementProjected); - w.Write("> __"); - w.Write(raw); - w.Write("_inlineArray);\n"); - w.Write(" "); - w.Write(elementProjected); - w.Write("[] __"); - w.Write(raw); - w.Write("_arrayFromPool = null;\n"); - w.Write(" Span<"); - w.Write(elementProjected); - w.Write("> __"); - w.Write(raw); - w.Write(" = __"); - w.Write(raw); - w.Write("Size <= 16\n ? __"); - w.Write(raw); - w.Write("_inlineArray[..(int)__"); - w.Write(raw); - w.Write("Size]\n : (__"); - w.Write(raw); - w.Write("_arrayFromPool = global::System.Buffers.ArrayPool<"); - w.Write(elementProjected); - w.Write(">.Shared.Rent((int)__"); - w.Write(raw); - w.Write("Size));\n"); - } - } - w.Write(" try\n {\n"); - - // For non-blittable PassArray params (read-only input arrays), emit CopyToManaged_ - // via UnsafeAccessor to convert the native ABI buffer into the managed Span the - // delegate sees. For FillArray params, the buffer is fresh storage the user delegate - // fills — the post-call writeback loop handles that. (Mirrors C++ which only emits the - // pre-call CopyToManaged for PassArray, see code_writers.h:7772 write_copy_to_managed.) - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); - string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); - // For complex structs, the data param is the ABI struct pointer (e.g. BasicStruct*). - // The Do_Abi parameter we receive is void* (per V3R3-M8), so the call-site needs an - // explicit (T*) cast to bridge the type. For ref-types (string/runtime-class/object), - // the data param is void** and the cast is (void**). - string dataParamType; - string dataCastExpr; - if (IsComplexStruct(szArr.BaseType)) - { - string abiStructName = GetAbiStructTypeName(w, szArr.BaseType); - dataParamType = abiStructName + "* data"; - dataCastExpr = "(" + abiStructName + "*)" + ptr; - } - else - { - dataParamType = "void** data"; - dataCastExpr = "(void**)" + ptr; - } - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToManaged\")]\n"); - w.Write(" static extern void CopyToManaged_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); - w.Write("\")] object _, uint length, "); - w.Write(dataParamType); - w.Write(", Span<"); - w.Write(elementProjected); - w.Write("> span);\n"); - w.Write(" CopyToManaged_"); - w.Write(raw); - w.Write("(null, __"); - w.Write(raw); - w.Write("Size, "); - w.Write(dataCastExpr); - w.Write(", __"); - w.Write(raw); - w.Write(");\n"); - } - - // For generic instance ABI input parameters, emit local UnsafeAccessor delegates and locals - // first so the call site can reference them. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - if (IsNullableT(p.Type)) - { - // Nullable param (server-side): use Marshaller.UnboxToManaged. Mirrors truth pattern. - string rawName = p.Parameter.Name ?? "param"; - string callName = Helpers.IsKeyword(rawName) ? "@" + rawName : rawName; - AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(p.Type)!; - string innerMarshaller = GetNullableInnerMarshallerName(w, inner); - w.Write(" var __arg_"); - w.Write(rawName); - w.Write(" = "); - w.Write(innerMarshaller); - w.Write(".UnboxToManaged("); - w.Write(callName); - w.Write(");\n"); - } - else if (IsGenericInstance(p.Type)) - { - string rawName = p.Parameter.Name ?? "param"; - string callName = Helpers.IsKeyword(rawName) ? "@" + rawName : rawName; - string interopTypeName = EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, p.Type, false))); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); - w.Write(" static extern "); - w.Write(projectedTypeName); - w.Write(" ConvertToManaged_arg_"); - w.Write(rawName); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, void* value);\n"); - w.Write(" var __arg_"); - w.Write(rawName); - w.Write(" = ConvertToManaged_arg_"); - w.Write(rawName); - w.Write("(null, "); - w.Write(callName); - w.Write(");\n"); - } - } - - if (returnIsString) - { - w.Write(" "); - w.Write(retLocalName); - w.Write(" = "); - } - else if (returnIsRefType) - { - w.Write(" "); - w.Write(retLocalName); - w.Write(" = "); - } - else if (returnIsReceiveArrayDoAbi) - { - // For T[] return: assign to existing local. - w.Write(" "); - w.Write(retLocalName); - w.Write(" = "); - } - else if (rt is not null) - { - w.Write(" "); - w.Write(retLocalName); - w.Write(" = "); - } - else - { - w.Write(" "); - } - - if (isGetter) - { - string propName = methodName.Substring(4); - w.Write("ComInterfaceDispatch.GetInstance<"); - w.Write(ifaceFullName); - w.Write(">((ComInterfaceDispatch*)thisPtr)."); - w.Write(propName); - w.Write(";\n"); - } - else if (isSetter) - { - string propName = methodName.Substring(4); - w.Write("ComInterfaceDispatch.GetInstance<"); - w.Write(ifaceFullName); - w.Write(">((ComInterfaceDispatch*)thisPtr)."); - w.Write(propName); - w.Write(" = "); - EmitDoAbiParamArgConversion(w, sig.Params[0]); - w.Write(";\n"); - } - else - { - w.Write("ComInterfaceDispatch.GetInstance<"); - w.Write(ifaceFullName); - w.Write(">((ComInterfaceDispatch*)thisPtr)."); - w.Write(methodName); - w.Write("("); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(",\n "); } - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat == ParamCategory.Out) - { - string raw = p.Parameter.Name ?? "param"; - w.Write("out __"); - w.Write(raw); - } - else if (cat == ParamCategory.Ref) - { - // WinRT 'in T' / 'ref const T' is a read-only by-ref input on the ABI side - // (pointer to a value the native caller owns). On the C# delegate / interface - // side it's projected as 'in T'. Read directly from * via the appropriate - // marshaller — DO NOT zero or write back. - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - AsmResolver.DotNet.Signatures.TypeSignature uRef = StripByRefAndCustomModifiers(p.Type); - if (IsString(uRef)) - { - w.Write("HStringMarshaller.ConvertToManaged(*"); - w.Write(ptr); - w.Write(")"); - } - else if (IsObject(uRef)) - { - w.Write("WindowsRuntimeObjectMarshaller.ConvertToManaged(*"); - w.Write(ptr); - w.Write(")"); - } - else if (IsRuntimeClassOrInterface(uRef)) - { - w.Write(GetMarshallerFullName(w, uRef)); - w.Write(".ConvertToManaged(*"); - w.Write(ptr); - w.Write(")"); - } - else if (IsMappedAbiValueType(uRef)) - { - w.Write(GetMappedMarshallerName(uRef)); - w.Write(".ConvertToManaged(*"); - w.Write(ptr); - w.Write(")"); - } - else if (IsHResultException(uRef)) - { - w.Write("global::ABI.System.ExceptionMarshaller.ConvertToManaged(*"); - w.Write(ptr); - w.Write(")"); - } - else if (IsComplexStruct(uRef)) - { - w.Write(GetMarshallerFullName(w, uRef)); - w.Write(".ConvertToManaged(*"); - w.Write(ptr); - w.Write(")"); - } - else if (IsAnyStruct(uRef) || IsBlittablePrimitive(uRef) || IsEnumType(uRef)) - { - // Blittable/almost-blittable: ABI layout matches projected layout. - w.Write("*"); - w.Write(ptr); - } - else - { - w.Write("*"); - w.Write(ptr); - } - } - else if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - { - string raw = p.Parameter.Name ?? "param"; - w.Write("__"); - w.Write(raw); - } - else if (cat == ParamCategory.ReceiveArray) - { - string raw = p.Parameter.Name ?? "param"; - w.Write("out __"); - w.Write(raw); - } - else - { - EmitDoAbiParamArgConversion(w, p); - } - } - w.Write(");\n"); - } - // After call: write back out params to caller's pointer. - // NOTE: Ref params (WinRT 'in T') are read-only inputs — never written back. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - AsmResolver.DotNet.Signatures.TypeSignature underlying = StripByRefAndCustomModifiers(p.Type); - w.Write(" *"); - w.Write(ptr); - w.Write(" = "); - // String: HStringMarshaller.ConvertToUnmanaged - if (IsString(underlying)) - { - w.Write("HStringMarshaller.ConvertToUnmanaged(__"); - w.Write(raw); - w.Write(")"); - } - // Object/runtime class: .ConvertToUnmanaged(...).DetachThisPtrUnsafe() - else if (IsObject(underlying)) - { - w.Write("WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(__"); - w.Write(raw); - w.Write(").DetachThisPtrUnsafe()"); - } - else if (IsRuntimeClassOrInterface(underlying)) - { - w.Write(GetMarshallerFullName(w, underlying)); - w.Write(".ConvertToUnmanaged(__"); - w.Write(raw); - w.Write(").DetachThisPtrUnsafe()"); - } - // Generic instance (e.g. IEnumerable): use the hoisted UnsafeAccessor - // 'ConvertToUnmanaged_' declared at the top of the method body. - else if (IsGenericInstance(underlying)) - { - w.Write("ConvertToUnmanaged_"); - w.Write(raw); - w.Write("(null, __"); - w.Write(raw); - w.Write(").DetachThisPtrUnsafe()"); - } - // For enums, function pointer signature uses the projected enum type, no cast needed. - // For bool, cast to byte. For char, cast to ushort. - else if (IsEnumType(underlying)) - { - w.Write("__"); - w.Write(raw); - } - else if (underlying is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibBool && - corlibBool.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) - { - w.Write("__"); - w.Write(raw); - } - else if (underlying is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibChar && - corlibChar.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) - { - w.Write("__"); - w.Write(raw); - } - // Non-blittable struct (e.g. authored BasicStruct with string fields): marshal - // the local managed value through Marshaller.ConvertToUnmanaged before - // writing it into the *out ABI struct slot. Mirrors C++ marshaler.write_marshal_from_managed - // (code_writers.h:7901-7910): "Marshaller.ConvertToUnmanaged(local)". - else if (IsComplexStruct(underlying)) - { - w.Write(GetMarshallerFullName(w, underlying)); - w.Write(".ConvertToUnmanaged(__"); - w.Write(raw); - w.Write(")"); - } - else - { - w.Write("__"); - w.Write(raw); - } - w.Write(";\n"); - } - // After call: for ReceiveArray params, emit ConvertToUnmanaged_ call (the - // [UnsafeAccessor] declaration was hoisted to the top of the method body). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.ReceiveArray) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(" ConvertToUnmanaged_"); - w.Write(raw); - w.Write("(null, __"); - w.Write(raw); - w.Write(", out *__"); - w.Write(raw); - w.Write("Size, out *"); - w.Write(ptr); - w.Write(");\n"); - } - // After call: for non-blittable FillArray params (Span where T is string/runtime - // class/object/non-blittable struct), copy the managed delegate's writes back into the - // native ABI buffer. Mirrors C++ write_marshal_from_managed (code_writers.h:7852-7869) - // which emits 'CopyToUnmanaged_(null, __, __Size, (T*))'. - // Blittable element types don't need this — the Span wraps the native buffer directly. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szFA) { continue; } - // Blittable element types: Span wraps the native buffer; no copy-back needed. - if (IsBlittablePrimitive(szFA.BaseType) || IsAnyStruct(szFA.BaseType)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string ptr = Helpers.IsKeyword(raw) ? "@" + raw : raw; - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szFA.BaseType)))); - string elementInteropArg = EncodeInteropTypeName(szFA.BaseType, TypedefNameType.Projected); - // Determine the ABI element type for the data pointer cast. - // - Strings / runtime classes / objects: void** - // - HResult exception: global::ABI.System.Exception* - // - Mapped value types (DateTime/TimeSpan): global::ABI.System.{DateTimeOffset/TimeSpan}* - // - Complex structs: * - string dataParamType; - string dataCastType; - if (IsString(szFA.BaseType) || IsRuntimeClassOrInterface(szFA.BaseType) || IsObject(szFA.BaseType)) - { - dataParamType = "void** data"; - dataCastType = "(void**)"; - } - else if (IsHResultException(szFA.BaseType)) - { - dataParamType = "global::ABI.System.Exception* data"; - dataCastType = "(global::ABI.System.Exception*)"; - } - else if (IsMappedAbiValueType(szFA.BaseType)) - { - string abiName = GetMappedAbiTypeName(szFA.BaseType); - dataParamType = abiName + "* data"; - dataCastType = "(" + abiName + "*)"; - } - else - { - string abiStructName = GetAbiStructTypeName(w, szFA.BaseType); - dataParamType = abiStructName + "* data"; - dataCastType = "(" + abiStructName + "*)"; - } - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToUnmanaged\")]\n"); - w.Write(" static extern void CopyToUnmanaged_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szFA.BaseType, elementInteropArg)); - w.Write("\")] object _, ReadOnlySpan<"); - w.Write(elementProjected); - w.Write("> span, uint length, "); - w.Write(dataParamType); - w.Write(");\n"); - w.Write(" CopyToUnmanaged_"); - w.Write(raw); - w.Write("(null, __"); - w.Write(raw); - w.Write(", __"); - w.Write(raw); - w.Write("Size, "); - w.Write(dataCastType); - w.Write(ptr); - w.Write(");\n"); - } - if (rt is not null) - { - if (returnIsHResultExceptionDoAbi) - { - w.Write(" *"); - w.Write(retParamName); - w.Write(" = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged("); - w.Write(retLocalName); - w.Write(");\n"); - } - else if (returnIsString) - { - w.Write(" *"); - w.Write(retParamName); - w.Write(" = HStringMarshaller.ConvertToUnmanaged("); - w.Write(retLocalName); - w.Write(");\n"); - } - else if (returnIsRefType) - { - if (rt is not null && IsNullableT(rt)) - { - // Nullable return (server-side): use Marshaller.BoxToUnmanaged. - AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(rt)!; - string innerMarshaller = GetNullableInnerMarshallerName(w, inner); - w.Write(" *"); - w.Write(retParamName); - w.Write(" = "); - w.Write(innerMarshaller); - w.Write(".BoxToUnmanaged("); - w.Write(retLocalName); - w.Write(").DetachThisPtrUnsafe();\n"); - } - else if (returnIsGenericInstance) - { - // Generic instance return: use the UnsafeAccessor static local function declared at - // the top of the method body via the M12 hoisting pass; just emit the call here. - w.Write(" *"); - w.Write(retParamName); - w.Write(" = ConvertToUnmanaged_"); - w.Write(retParamName); - w.Write("(null, "); - w.Write(retLocalName); - w.Write(").DetachThisPtrUnsafe();\n"); - } - else - { - w.Write(" *"); - w.Write(retParamName); - w.Write(" = "); - EmitMarshallerConvertToUnmanaged(w, rt!, retLocalName); - w.Write(".DetachThisPtrUnsafe();\n"); - } - } - else if (returnIsReceiveArrayDoAbi) - { - // Return-receive-array: emit ConvertToUnmanaged_ call (declaration - // was hoisted to the top of the method body). - w.Write(" ConvertToUnmanaged_"); - w.Write(retParamName); - w.Write("(null, "); - w.Write(retLocalName); - w.Write(", out *"); - w.Write(retSizeParamName); - w.Write(", out *"); - w.Write(retParamName); - w.Write(");\n"); - } - else if (IsMappedAbiValueType(rt)) - { - // Mapped value type return (DateTime/TimeSpan): convert via marshaller. - w.Write(" *"); - w.Write(retParamName); - w.Write(" = "); - w.Write(GetMappedMarshallerName(rt)); - w.Write(".ConvertToUnmanaged("); - w.Write(retLocalName); - w.Write(");\n"); - } - else if (IsSystemType(rt)) - { - // System.Type return (server-side): convert managed System.Type to ABI Type struct. - w.Write(" *"); - w.Write(retParamName); - w.Write(" = global::ABI.System.TypeMarshaller.ConvertToUnmanaged("); - w.Write(retLocalName); - w.Write(");\n"); - } - else if (IsComplexStruct(rt)) - { - // Complex struct return (server-side): convert managed struct to ABI struct via marshaller. - w.Write(" *"); - w.Write(retParamName); - w.Write(" = "); - w.Write(GetMarshallerFullName(w, rt)); - w.Write(".ConvertToUnmanaged("); - w.Write(retLocalName); - w.Write(");\n"); - } - else if (returnIsBlittableStruct) - { - w.Write(" *"); - w.Write(retParamName); - w.Write(" = "); - w.Write(retLocalName); - w.Write(";\n"); - } - else - { - string abiType = GetAbiPrimitiveType(rt); - w.Write(" *"); - w.Write(retParamName); - w.Write(" = "); - if (rt is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && - corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) - { - w.Write(retLocalName); - w.Write(";\n"); - } - else if (rt is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib2 && - corlib2.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) - { - w.Write(retLocalName); - w.Write(";\n"); - } - else if (IsEnumType(rt)) - { - // Enum: function pointer signature uses the projected enum type, no cast needed. - w.Write(retLocalName); - w.Write(";\n"); - } - else - { - w.Write(retLocalName); - w.Write(";\n"); - } - } - } - w.Write(" return 0;\n }\n"); - w.Write(" catch (Exception __exception__)\n {\n"); - w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__);\n }\n"); - - // For non-blittable PassArray params, emit finally block with ArrayPool.Shared.Return. - bool hasNonBlittableArrayDoAbi = false; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - hasNonBlittableArrayDoAbi = true; - break; - } - if (hasNonBlittableArrayDoAbi) - { - w.Write(" finally\n {\n"); - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); - w.Write("\n if (__"); - w.Write(raw); - w.Write("_arrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool<"); - w.Write(elementProjected); - w.Write(">.Shared.Return(__"); - w.Write(raw); - w.Write("_arrayFromPool);\n }\n"); - } - w.Write(" }\n"); - } - - w.Write("}\n\n"); - _ = hasStringParams; - } - - /// Converts an ABI parameter to its projected (managed) form for the Do_Abi call. - private static void EmitDoAbiParamArgConversion(TypeWriter w, ParamInfo p) - { - string rawName = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(rawName) ? "@" + rawName : rawName; - if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && - corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) - { - w.Write(pname); - } - else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib2 && - corlib2.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) - { - w.Write(pname); - } - else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibStr && - corlibStr.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String) - { - w.Write("HStringMarshaller.ConvertToManaged("); - w.Write(pname); - w.Write(")"); - } - else if (IsGenericInstance(p.Type)) - { - // Generic instance ABI parameter: caller already declared a local UnsafeAccessor + - // local var __arg_ that holds the converted value. - w.Write("__arg_"); - w.Write(rawName); - } - else if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type)) - { - EmitMarshallerConvertToManaged(w, p.Type, pname); - } - else if (IsMappedAbiValueType(p.Type)) - { - // Mapped value type input (DateTime/TimeSpan): the parameter is the ABI type; - // convert to the projected managed type via the marshaller. - w.Write(GetMappedMarshallerName(p.Type)); - w.Write(".ConvertToManaged("); - w.Write(pname); - w.Write(")"); - } - else if (IsSystemType(p.Type)) - { - // System.Type input (server-side): convert ABI Type struct to System.Type. - w.Write("global::ABI.System.TypeMarshaller.ConvertToManaged("); - w.Write(pname); - w.Write(")"); - } - else if (IsComplexStruct(p.Type)) - { - // Complex struct input (server-side): convert ABI struct to managed via marshaller. - w.Write(GetMarshallerFullName(w, p.Type)); - w.Write(".ConvertToManaged("); - w.Write(pname); - w.Write(")"); - } - else if (IsAnyStruct(p.Type)) - { - // Blittable / almost-blittable struct: pass directly (projected type == ABI type). - w.Write(pname); - } - else if (IsEnumType(p.Type)) - { - // Enum: param signature is already the projected enum type, no cast needed. - w.Write(pname); - } - else - { - w.Write(pname); - } - } - - /// Mirrors C++ write_interface_idic_impl. - public static void WriteInterfaceIdicImpl(TypeWriter w, TypeDefinition type) - { - if (TypeCategorization.IsExclusiveTo(type) && !w.Settings.IdicExclusiveTo) { return; } - if (type.GenericParameters.Count > 0) { return; } - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - w.Write("\n[DynamicInterfaceCastableImplementation]\n"); - WriteGuidAttribute(w, type); - w.Write("\n"); - w.Write("file interface "); - w.Write(nameStripped); - w.Write(" : "); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write("\n{\n"); - // Emit DIM bodies that dispatch through the static ABI Methods class. - WriteInterfaceIdicImplMembers(w, type); - w.Write("\n}\n"); - } - - /// - /// Emits explicit-interface DIM (default interface method) implementations for the IDIC - /// file interface. Mirrors C++ write_interface_members. - /// - private static void WriteInterfaceIdicImplMembers(TypeWriter w, TypeDefinition type) - { - HashSet visited = new(); - WriteInterfaceIdicImplMembersForInterface(w, type, visited); - - // Also walk required (inherited) interfaces and emit members for each one. - // Mirrors C++ write_required_interface_members_for_abi_type. - WriteInterfaceIdicImplMembersForRequiredInterfaces(w, type, visited); - } - - private static void WriteInterfaceIdicImplMembersForRequiredInterfaces( - TypeWriter w, TypeDefinition type, HashSet visited) - { - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - TypeDefinition? required = ResolveInterfaceTypeDef(impl.Interface); - if (required is null) { continue; } - if (!visited.Add(required)) { continue; } - string rNs = required.Namespace?.Value ?? string.Empty; - string rName = required.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(rNs, rName); - if (mapped is not null && mapped.HasCustomMembersOutput) - { - // Mapped to a BCL interface (IBindableVector -> IList, IBindableIterable -> IEnumerable, etc.). - // Emit explicit-interface DIM forwarders for the BCL members so the DIC shim - // satisfies them when queried via casts like '((IList)(WindowsRuntimeObject)this)'. - EmitDicShimMappedBclForwarders(w, rName); - // IBindableVector's IList forwarders already include the IEnumerable.GetEnumerator - // forwarder (since IList : IEnumerable). Pre-add IBindableIterable to the visited - // set so we don't emit a second GetEnumerator forwarder for it. We also walk the - // required interfaces so any other (deeper) inherited mapped interface is covered. - if (rName == "IBindableVector") - { - foreach (InterfaceImplementation impl2 in required.Interfaces) - { - if (impl2.Interface is null) { continue; } - TypeDefinition? r2 = ResolveInterfaceTypeDef(impl2.Interface); - if (r2 is not null) { visited.Add(r2); } - } - } - continue; - } - // Special case: IObservableMap`2 and IObservableVector`1 are NOT mapped to BCL - // interfaces (they retain WinRT names) but they DO need to forward their inherited - // IDictionary/IList members for cast-based dispatch. Mirrors C++ which uses - // write_dictionary_members_using_idic / write_list_members_using_idic when walking - // these interfaces in write_required_interface_members_for_abi_type. - if (rNs == "Windows.Foundation.Collections" && rName == "IObservableMap`2") - { - if (impl.Interface is TypeSpecification tsMap && tsMap.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature giMap && giMap.TypeArguments.Count == 2) - { - string keyText = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(giMap.TypeArguments[0]), TypedefNameType.Projected, true))); - string valueText = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(giMap.TypeArguments[1]), TypedefNameType.Projected, true))); - EmitDicShimIObservableMapForwarders(w, keyText, valueText); - // Mark the inherited IMap`2 / IIterable`1 as visited so they aren't re-emitted. - foreach (InterfaceImplementation impl2 in required.Interfaces) - { - if (impl2.Interface is null) { continue; } - TypeDefinition? r2 = ResolveInterfaceTypeDef(impl2.Interface); - if (r2 is not null) { visited.Add(r2); } - } - } - continue; - } - if (rNs == "Windows.Foundation.Collections" && rName == "IObservableVector`1") - { - if (impl.Interface is TypeSpecification tsVec && tsVec.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature giVec && giVec.TypeArguments.Count == 1) - { - string elementText = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(giVec.TypeArguments[0]), TypedefNameType.Projected, true))); - EmitDicShimIObservableVectorForwarders(w, elementText); - foreach (InterfaceImplementation impl2 in required.Interfaces) - { - if (impl2.Interface is null) { continue; } - TypeDefinition? r2 = ResolveInterfaceTypeDef(impl2.Interface); - if (r2 is not null) { visited.Add(r2); } - } - } - continue; - } - // Skip generic interfaces with unbound params (we can't substitute T at this layer). - if (required.GenericParameters.Count > 0) { continue; } - // Recurse first so deepest-base is emitted before nearer-base (matches deduplication). - WriteInterfaceIdicImplMembersForRequiredInterfaces(w, required, visited); - WriteInterfaceIdicImplMembersForInheritedInterface(w, required); - } - } - - /// - /// Emits IDictionary<K,V> / ICollection<KVP> / IEnumerable<KVP> + - /// IObservableMap<K,V>.MapChanged forwarders for a DIC file interface that inherits - /// from Windows.Foundation.Collections.IObservableMap<K,V>. Mirrors C++ - /// write_dictionary_members_using_idic(true) + the IObservableMap event forwarder. - /// - private static void EmitDicShimIObservableMapForwarders(TypeWriter w, string keyText, string valueText) - { - string target = $"((global::System.Collections.Generic.IDictionary<{keyText}, {valueText}>)(WindowsRuntimeObject)this)"; - string self = $"global::System.Collections.Generic.IDictionary<{keyText}, {valueText}>."; - string icoll = $"global::System.Collections.Generic.ICollection>."; - w.Write("\n"); - w.Write($"ICollection<{keyText}> {self}Keys => {target}.Keys;\n"); - w.Write($"ICollection<{valueText}> {self}Values => {target}.Values;\n"); - w.Write($"int {icoll}Count => {target}.Count;\n"); - w.Write($"bool {icoll}IsReadOnly => {target}.IsReadOnly;\n"); - w.Write($"{valueText} {self}this[{keyText} key] \n"); - w.Write("{\n"); - w.Write($"get => {target}[key];\n"); - w.Write($"set => {target}[key] = value;\n"); - w.Write("}\n"); - w.Write($"void {self}Add({keyText} key, {valueText} value) => {target}.Add(key, value);\n"); - w.Write($"bool {self}ContainsKey({keyText} key) => {target}.ContainsKey(key);\n"); - w.Write($"bool {self}Remove({keyText} key) => {target}.Remove(key);\n"); - w.Write($"bool {self}TryGetValue({keyText} key, out {valueText} value) => {target}.TryGetValue(key, out value);\n"); - w.Write($"void {icoll}Add(KeyValuePair<{keyText}, {valueText}> item) => {target}.Add(item);\n"); - w.Write($"void {icoll}Clear() => {target}.Clear();\n"); - w.Write($"bool {icoll}Contains(KeyValuePair<{keyText}, {valueText}> item) => {target}.Contains(item);\n"); - w.Write($"void {icoll}CopyTo(KeyValuePair<{keyText}, {valueText}>[] array, int arrayIndex) => {target}.CopyTo(array, arrayIndex);\n"); - w.Write($"bool ICollection>.Remove(KeyValuePair<{keyText}, {valueText}> item) => {target}.Remove(item);\n"); - // Enumerable forwarders. - w.Write("\n"); - w.Write($"IEnumerator> IEnumerable>.GetEnumerator() => {target}.GetEnumerator();\n"); - w.Write("IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n"); - // IObservableMap.MapChanged event forwarder. - string obsTarget = $"((global::Windows.Foundation.Collections.IObservableMap<{keyText}, {valueText}>)(WindowsRuntimeObject)this)"; - string obsSelf = $"global::Windows.Foundation.Collections.IObservableMap<{keyText}, {valueText}>."; - w.Write("\n"); - w.Write($"event global::Windows.Foundation.Collections.MapChangedEventHandler<{keyText}, {valueText}> {obsSelf}MapChanged\n"); - w.Write("{\n"); - w.Write($"add => {obsTarget}.MapChanged += value;\n"); - w.Write($"remove => {obsTarget}.MapChanged -= value;\n"); - w.Write("}\n"); - } - - /// - /// Emits IList<T> / ICollection<T> / IEnumerable<T> + - /// IObservableVector<T>.VectorChanged forwarders for a DIC file interface that inherits - /// from Windows.Foundation.Collections.IObservableVector<T>. Mirrors C++ - /// write_list_members_using_idic(true) + the IObservableVector event forwarder. - /// - private static void EmitDicShimIObservableVectorForwarders(TypeWriter w, string elementText) - { - string target = $"((global::System.Collections.Generic.IList<{elementText}>)(WindowsRuntimeObject)this)"; - string self = $"global::System.Collections.Generic.IList<{elementText}>."; - string icoll = $"global::System.Collections.Generic.ICollection<{elementText}>."; - w.Write("\n"); - w.Write($"int {icoll}Count => {target}.Count;\n"); - w.Write($"bool {icoll}IsReadOnly => {target}.IsReadOnly;\n"); - w.Write($"{elementText} {self}this[int index]\n"); - w.Write("{\n"); - w.Write($"get => {target}[index];\n"); - w.Write($"set => {target}[index] = value;\n"); - w.Write("}\n"); - w.Write($"int {self}IndexOf({elementText} item) => {target}.IndexOf(item);\n"); - w.Write($"void {self}Insert(int index, {elementText} item) => {target}.Insert(index, item);\n"); - w.Write($"void {self}RemoveAt(int index) => {target}.RemoveAt(index);\n"); - w.Write($"void {icoll}Add({elementText} item) => {target}.Add(item);\n"); - w.Write($"void {icoll}Clear() => {target}.Clear();\n"); - w.Write($"bool {icoll}Contains({elementText} item) => {target}.Contains(item);\n"); - w.Write($"void {icoll}CopyTo({elementText}[] array, int arrayIndex) => {target}.CopyTo(array, arrayIndex);\n"); - w.Write($"bool {icoll}Remove({elementText} item) => {target}.Remove(item);\n"); - w.Write("\n"); - w.Write($"IEnumerator<{elementText}> IEnumerable<{elementText}>.GetEnumerator() => {target}.GetEnumerator();\n"); - w.Write("IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n"); - // IObservableVector.VectorChanged event forwarder. - string obsTarget = $"((global::Windows.Foundation.Collections.IObservableVector<{elementText}>)(WindowsRuntimeObject)this)"; - string obsSelf = $"global::Windows.Foundation.Collections.IObservableVector<{elementText}>."; - w.Write("\n"); - w.Write($"event global::Windows.Foundation.Collections.VectorChangedEventHandler<{elementText}> {obsSelf}VectorChanged\n"); - w.Write("{\n"); - w.Write($"add => {obsTarget}.VectorChanged += value;\n"); - w.Write($"remove => {obsTarget}.VectorChanged -= value;\n"); - w.Write("}\n"); - } - - /// - /// Emits explicit-interface DIM thunks for an *inherited* (required) interface on a DIC - /// file interface shim. Each member becomes a thin - /// => ((IParent)(WindowsRuntimeObject)this).Member delegating thunk so that DIC - /// re-dispatches through the parent's own DIC shim. Mirrors the C++ tool's emission for - /// inherited-interface members in DIC shims. - /// - private static void WriteInterfaceIdicImplMembersForInheritedInterface(TypeWriter w, TypeDefinition type) - { - // The CCW interface name (the projected interface name with global:: prefix). For the - // delegating thunks we cast through this same projected interface type. - string ccwIfaceName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); - if (!ccwIfaceName.StartsWith("global::", System.StringComparison.Ordinal)) { ccwIfaceName = "global::" + ccwIfaceName; } - - foreach (MethodDefinition method in type.Methods) - { - if (Helpers.IsSpecial(method)) { continue; } - MethodSig sig = new(method); - string mname = method.Name?.Value ?? string.Empty; - - w.Write("\n"); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(ccwIfaceName); - w.Write("."); - w.Write(mname); - w.Write("("); - WriteParameterList(w, sig); - w.Write(") => (("); - w.Write(ccwIfaceName); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(mname); - w.Write("("); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(", "); } - WriteParameterNameWithModifier(w, sig.Params[i]); - } - w.Write(");\n"); - } - - foreach (PropertyDefinition prop in type.Properties) - { - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - string pname = prop.Name?.Value ?? string.Empty; - string propType = WritePropType(w, prop); - - w.Write("\n"); - w.Write(propType); - w.Write(" "); - w.Write(ccwIfaceName); - w.Write("."); - w.Write(pname); - if (getter is not null && setter is null) - { - // Read-only: single-line expression body. - w.Write(" => (("); - w.Write(ccwIfaceName); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(pname); - w.Write(";\n"); - } - else - { - w.Write("\n{\n"); - if (getter is not null) - { - w.Write(" get => (("); - w.Write(ccwIfaceName); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(pname); - w.Write(";\n"); - } - if (setter is not null) - { - w.Write(" set => (("); - w.Write(ccwIfaceName); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(pname); - w.Write(" = value;\n"); - } - w.Write("}\n"); - } - } - - foreach (EventDefinition evt in type.Events) - { - string evtName = evt.Name?.Value ?? string.Empty; - w.Write("\nevent "); - WriteEventType(w, evt); - w.Write(" "); - w.Write(ccwIfaceName); - w.Write("."); - w.Write(evtName); - w.Write("\n{\n"); - w.Write(" add => (("); - w.Write(ccwIfaceName); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(evtName); - w.Write(" += value;\n"); - w.Write(" remove => (("); - w.Write(ccwIfaceName); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(evtName); - w.Write(" -= value;\n"); - w.Write("}\n"); - } - } - - /// - /// Emits explicit-interface DIM forwarders on a DIC file interface shim for the BCL - /// members that come from a system-collection-mapped required WinRT interface - /// (e.g. IBindableVector maps to IList, so we must satisfy IList, - /// ICollection, and IEnumerable members on the shim). The forwarders all - /// re-cast through (WindowsRuntimeObject)this so the DIC machinery can re-dispatch - /// to the real BCL adapter shim. - /// - private static void EmitDicShimMappedBclForwarders(TypeWriter w, string mappedWinRTInterfaceName) - { - switch (mappedWinRTInterfaceName) - { - case "IClosable": - // IClosable maps to IDisposable. Forward Dispose() to the - // WindowsRuntimeObject base which has the actual implementation. - w.Write("\nvoid global::System.IDisposable.Dispose() => ((global::System.IDisposable)(WindowsRuntimeObject)this).Dispose();\n"); - break; - case "IBindableVector": - // IList covers IList, ICollection, and IEnumerable members. - w.Write("\n"); - w.Write("int global::System.Collections.ICollection.Count => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Count;\n"); - w.Write("bool global::System.Collections.ICollection.IsSynchronized => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsSynchronized;\n"); - w.Write("object global::System.Collections.ICollection.SyncRoot => ((global::System.Collections.IList)(WindowsRuntimeObject)this).SyncRoot;\n"); - w.Write("void global::System.Collections.ICollection.CopyTo(Array array, int index) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).CopyTo(array, index);\n\n"); - w.Write("object global::System.Collections.IList.this[int index]\n{\n"); - w.Write("get => ((global::System.Collections.IList)(WindowsRuntimeObject)this)[index];\n"); - w.Write("set => ((global::System.Collections.IList)(WindowsRuntimeObject)this)[index] = value;\n}\n"); - w.Write("bool global::System.Collections.IList.IsFixedSize => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsFixedSize;\n"); - w.Write("bool global::System.Collections.IList.IsReadOnly => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsReadOnly;\n"); - w.Write("int global::System.Collections.IList.Add(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Add(value);\n"); - w.Write("void global::System.Collections.IList.Clear() => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Clear();\n"); - w.Write("bool global::System.Collections.IList.Contains(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Contains(value);\n"); - w.Write("int global::System.Collections.IList.IndexOf(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IndexOf(value);\n"); - w.Write("void global::System.Collections.IList.Insert(int index, object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Insert(index, value);\n"); - w.Write("void global::System.Collections.IList.Remove(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Remove(value);\n"); - w.Write("void global::System.Collections.IList.RemoveAt(int index) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).RemoveAt(index);\n\n"); - w.Write("IEnumerator IEnumerable.GetEnumerator() => ((global::System.Collections.IList)(WindowsRuntimeObject)this).GetEnumerator();\n"); - break; - case "IBindableIterable": - w.Write("\n"); - w.Write("IEnumerator IEnumerable.GetEnumerator() => ((global::System.Collections.IEnumerable)(WindowsRuntimeObject)this).GetEnumerator();\n"); - break; - } - } - - private static void WriteInterfaceIdicImplMembersForInterface(TypeWriter w, TypeDefinition type, HashSet visited) - { - // The CCW interface name (the projected interface name with global:: prefix). - string ccwIfaceName = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.Projected, true))); - if (!ccwIfaceName.StartsWith("global::", System.StringComparison.Ordinal)) { ccwIfaceName = "global::" + ccwIfaceName; } - // The static ABI Methods class name. - string abiClass = w.WriteTemp("%", new System.Action(_ => WriteTypedefName(w, type, TypedefNameType.StaticAbiClass, true))); - if (!abiClass.StartsWith("global::", System.StringComparison.Ordinal)) { abiClass = "global::" + abiClass; } - - foreach (MethodDefinition method in type.Methods) - { - if (Helpers.IsSpecial(method)) { continue; } - MethodSig sig = new(method); - string mname = method.Name?.Value ?? string.Empty; - - w.Write("\nunsafe "); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(ccwIfaceName); - w.Write("."); - w.Write(mname); - w.Write("("); - WriteParameterList(w, sig); - w.Write(")\n{\n"); - w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); - w.Write(ccwIfaceName); - w.Write(").TypeHandle);\n "); - if (sig.ReturnType is not null) { w.Write("return "); } - w.Write(abiClass); - w.Write("."); - w.Write(mname); - w.Write("(_obj"); - for (int i = 0; i < sig.Params.Count; i++) - { - w.Write(", "); - WriteParameterNameWithModifier(w, sig.Params[i]); - } - w.Write(");\n}\n"); - } - - foreach (PropertyDefinition prop in type.Properties) - { - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - string pname = prop.Name?.Value ?? string.Empty; - string propType = WritePropType(w, prop); - - w.Write("\nunsafe "); - w.Write(propType); - w.Write(" "); - w.Write(ccwIfaceName); - w.Write("."); - w.Write(pname); - w.Write("\n{\n"); - if (getter is not null) - { - w.Write(" get\n {\n"); - w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); - w.Write(ccwIfaceName); - w.Write(").TypeHandle);\n"); - w.Write(" return "); - w.Write(abiClass); - w.Write("."); - w.Write(pname); - w.Write("(_obj);\n }\n"); - } - if (setter is not null) - { - // If the property has only a setter on this interface BUT a base interface declares - // the getter (so the C# interface decl emits 'get; set;'), C# requires an explicit - // interface impl to provide both accessors. Emit a synthetic getter that delegates - // to the base interface where the getter actually lives. Mirrors C++ - // code_writers.h:7052-7062. - if (getter is null) - { - TypeDefinition? baseIfaceWithGetter = FindPropertyInterfaceInBases(type, pname); - if (baseIfaceWithGetter is not null) - { - w.Write(" get { return (("); - WriteInterfaceTypeNameForCcw(w, baseIfaceWithGetter); - w.Write(")(WindowsRuntimeObject)this)."); - w.Write(pname); - w.Write("; }\n"); - } - } - w.Write(" set\n {\n"); - w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); - w.Write(ccwIfaceName); - w.Write(").TypeHandle);\n"); - w.Write(" "); - w.Write(abiClass); - w.Write("."); - w.Write(pname); - w.Write("(_obj, value);\n }\n"); - } - w.Write("}\n"); - } - - // Events: emit explicit interface event implementations on the IDIC interface that - // dispatch through the static ABI Methods class's event accessor (returns an EventSource). - // Mirrors C++ write_interface_members event handling (calls EventName(thisRef, _obj).Subscribe/Unsubscribe). - foreach (EventDefinition evt in type.Events) - { - string evtName = evt.Name?.Value ?? string.Empty; - w.Write("\nevent "); - WriteEventType(w, evt); - w.Write(" "); - w.Write(ccwIfaceName); - w.Write("."); - w.Write(evtName); - w.Write("\n{\n"); - // add accessor - w.Write(" add\n {\n"); - w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); - w.Write(ccwIfaceName); - w.Write(").TypeHandle);\n "); - w.Write(abiClass); - w.Write("."); - w.Write(evtName); - w.Write("((WindowsRuntimeObject)this, _obj).Subscribe(value);\n }\n"); - // remove accessor - w.Write(" remove\n {\n"); - w.Write(" var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof("); - w.Write(ccwIfaceName); - w.Write(").TypeHandle);\n "); - w.Write(abiClass); - w.Write("."); - w.Write(evtName); - w.Write("((WindowsRuntimeObject)this, _obj).Unsubscribe(value);\n }\n"); - w.Write("}\n"); - } - } - - /// Mirrors C++ write_interface_marshaller. - public static void WriteInterfaceMarshaller(TypeWriter w, TypeDefinition type) - { - if (TypeCategorization.IsExclusiveTo(type)) { return; } - if (type.GenericParameters.Count > 0) { return; } - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - - w.Write("\n#nullable enable\n"); - w.Write("public static unsafe class "); - w.Write(nameStripped); - w.Write("Marshaller\n{\n"); - w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write(" value)\n {\n"); - w.Write(" return WindowsRuntimeInterfaceMarshaller<"); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write(">.ConvertToUnmanaged(value, "); - WriteIidGuidReference(w, type); - w.Write(");\n }\n\n"); - w.Write(" public static "); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write("? ConvertToManaged(void* value)\n {\n"); - w.Write(" return ("); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write("?) WindowsRuntimeObjectMarshaller.ConvertToManaged(value);\n }\n}\n"); - w.Write("#nullable disable\n"); - } - - /// Mirrors C++ write_iid_guid for use by ABI helpers. - public static void WriteIidGuidReference(TypeWriter w, TypeDefinition type) - { - if (type.GenericParameters.Count != 0) - { - // Generic interface IID - call the unsafe accessor - WriteIidGuidPropertyName(w, type); - w.Write("(null)"); - return; - } - string ns = type.Namespace?.Value ?? string.Empty; - string nm = type.Name?.Value ?? string.Empty; - if (MappedTypes.Get(ns, nm) is { } m && m.MappedName == "IStringable") - { - w.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable"); - return; - } - w.Write("global::ABI.InterfaceIIDs."); - WriteIidGuidPropertyName(w, type); - } - - /// - /// Writes a marshaller class for a struct or enum (mirrors C++ write_struct_and_enum_marshaller_class). - /// - private static void WriteStructEnumMarshallerClass(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - TypeCategory cat = TypeCategorization.GetCategory(type); - bool blittable = IsTypeBlittable(type); - // "Almost-blittable" includes blittable + bool/char fields. Excludes string/object fields. - // Use the same predicate as IsAnyStruct (which is now scoped to almost-blittable). - AsmResolver.DotNet.Signatures.TypeDefOrRefSignature sig = type.ToTypeSignature(false) is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td2 ? td2 : null!; - bool almostBlittable = cat == TypeCategory.Struct && (sig is null || IsAnyStruct(sig)); - bool isEnum = cat == TypeCategory.Enum; - // Complex structs are non-almost-blittable structs with reference fields (string, object, etc.). - bool isComplexStruct = cat == TypeCategory.Struct && !almostBlittable; - // Detect Nullable reference fields to determine whether the struct's BoxToUnmanaged - // call needs CreateComInterfaceFlags.TrackerSupport (mirrors C++ use_tracker_object_support - // which returns true for IReference`1 generic instances). - bool hasReferenceFields = false; - if (isComplexStruct) - { - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - if (TryGetNullablePrimitiveMarshallerName(ft, out _)) { hasReferenceFields = true; } - } - } - - // For structs that are mapped (e.g. Duration, KeyTime, RepeatBehavior — they have - // EmitAbi=true and an addition file that completely replaces the public struct), skip - // the per-field ConvertToUnmanaged/ConvertToManaged because the projected struct's - // public fields don't match the WinMD field layout. The truth marshaller for these - // contains only BoxToUnmanaged/UnboxToManaged. - string typeNs = type.Namespace?.Value ?? string.Empty; - string typeNm = type.Name?.Value ?? string.Empty; - bool isMappedStruct = isComplexStruct && MappedTypes.Get(typeNs, typeNm) is not null; - if (isMappedStruct) { isComplexStruct = false; } - - w.Write("public static unsafe class "); - w.Write(nameStripped); - w.Write("Marshaller\n{\n"); - - if (isComplexStruct) - { - // ConvertToUnmanaged: build ABI struct from projected struct via per-field marshalling. - w.Write(" public static "); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write(" ConvertToUnmanaged("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(" value)\n {\n"); - w.Write(" return new() {\n"); - bool first = true; - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - string fname = field.Name?.Value ?? ""; - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - if (!first) { w.Write(",\n"); } - first = false; - w.Write(" "); - w.Write(fname); - w.Write(" = "); - if (IsString(ft)) - { - w.Write("HStringMarshaller.ConvertToUnmanaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (IsMappedAbiValueType(ft)) - { - w.Write(GetMappedMarshallerName(ft)); - w.Write(".ConvertToUnmanaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (IsHResultException(ft)) - { - // Mapped value type 'HResult' (excluded from IsMappedAbiValueType because - // it's "treated specially in many places", but for nested struct fields the - // marshalling is identical: use ABI.System.ExceptionMarshaller). - w.Write("global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature ftd - && TryResolveStructTypeDef(ftd) is TypeDefinition fieldStructTd - && TypeCategorization.GetCategory(fieldStructTd) == TypeCategory.Struct - && !IsTypeBlittable(fieldStructTd)) - { - // Nested non-blittable struct: marshal via its Marshaller. - w.Write(Helpers.StripBackticks(fieldStructTd.Name?.Value ?? string.Empty)); - w.Write("Marshaller.ConvertToUnmanaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (TryGetNullablePrimitiveMarshallerName(ft, out string? nullableMarshaller)) - { - w.Write(nullableMarshaller!); - w.Write(".BoxToUnmanaged(value."); - w.Write(fname); - w.Write(").DetachThisPtrUnsafe()"); - } - else - { - w.Write("value."); - w.Write(fname); - } - } - w.Write("\n };\n }\n"); - - // ConvertToManaged: construct projected struct via constructor accepting the marshalled fields. - w.Write(" public static "); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(" ConvertToManaged("); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - // Mirror C++ write_convert_to_managed_method_struct (code_writers.h:4536-4540): - // - In component mode: emit object initializer with named field assignments - // (positional ctor not always available on authored types). - // - In non-component mode: emit positional constructor (matches the auto-generated - // primary constructor on projected struct types). - bool useObjectInitializer = w.Settings.Component; - w.Write(" value)\n {\n"); - w.Write(" return new "); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(useObjectInitializer ? "(){\n" : "(\n"); - first = true; - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - string fname = field.Name?.Value ?? ""; - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - if (!first) { w.Write(",\n"); } - first = false; - w.Write(" "); - if (useObjectInitializer) - { - w.Write(fname); - w.Write(" = "); - } - if (IsString(ft)) - { - w.Write("HStringMarshaller.ConvertToManaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (IsMappedAbiValueType(ft)) - { - w.Write(GetMappedMarshallerName(ft)); - w.Write(".ConvertToManaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (IsHResultException(ft)) - { - // Mapped value type 'HResult' (excluded from IsMappedAbiValueType because - // it's "treated specially in many places", but for nested struct fields the - // marshalling is identical: use ABI.System.ExceptionMarshaller). - w.Write("global::ABI.System.ExceptionMarshaller.ConvertToManaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature ftd2 - && TryResolveStructTypeDef(ftd2) is TypeDefinition fieldStructTd2 - && TypeCategorization.GetCategory(fieldStructTd2) == TypeCategory.Struct - && !IsTypeBlittable(fieldStructTd2)) - { - // Nested non-blittable struct: convert via its Marshaller. - w.Write(Helpers.StripBackticks(fieldStructTd2.Name?.Value ?? string.Empty)); - w.Write("Marshaller.ConvertToManaged(value."); - w.Write(fname); - w.Write(")"); - } - else if (TryGetNullablePrimitiveMarshallerName(ft, out string? nullableMarshaller)) - { - w.Write(nullableMarshaller!); - w.Write(".UnboxToManaged(value."); - w.Write(fname); - w.Write(")"); - } - else - { - w.Write("value."); - w.Write(fname); - } - } - w.Write(useObjectInitializer ? "\n };\n }\n" : "\n );\n }\n"); - - // Dispose: free non-blittable fields. - w.Write(" public static void Dispose("); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write(" value)\n {\n"); - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - string fname = field.Name?.Value ?? ""; - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - if (IsString(ft)) - { - w.Write(" HStringMarshaller.Free(value."); - w.Write(fname); - w.Write(");\n"); - } - else if (IsHResultException(ft)) - { - // HResult/Exception field has no per-value resources to release - // (the ABI representation is just an int HRESULT). Skip Dispose entirely. - continue; - } - else if (IsMappedAbiValueType(ft)) - { - // Mapped value types (DateTime/TimeSpan) have no per-value resources to - // release — the ABI representation is just an int64. Mirror C++ - // set_skip_disposer_if_needed (code_writers.h:6431-6440) which explicitly - // skips the disposer for global::ABI.System.{DateTimeOffset,TimeSpan,Exception}. - continue; - } - else if (ft is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature ftd3 - && TryResolveStructTypeDef(ftd3) is TypeDefinition fieldStructTd3 - && TypeCategorization.GetCategory(fieldStructTd3) == TypeCategory.Struct - && !IsTypeBlittable(fieldStructTd3)) - { - // Nested non-blittable struct: dispose via its Marshaller. - // Mirror C++: this site always uses the fully-qualified marshaller name. - string nestedNs = fieldStructTd3.Namespace?.Value ?? string.Empty; - string nestedNm = Helpers.StripBackticks(fieldStructTd3.Name?.Value ?? string.Empty); - w.Write(" global::ABI."); - w.Write(nestedNs); - w.Write("."); - w.Write(nestedNm); - w.Write("Marshaller.Dispose(value."); - w.Write(fname); - w.Write(");\n"); - } - else if (TryGetNullablePrimitiveMarshallerName(ft, out _)) - { - w.Write(" WindowsRuntimeUnknownMarshaller.Free(value."); - w.Write(fname); - w.Write(");\n"); - } - } - w.Write(" }\n"); - } - - // BoxToUnmanaged: same pattern for all (enum, almost-blittable, complex). - // Truth uses CreateComInterfaceFlags.TrackerSupport when the struct has reference type - // fields (Nullable, etc.) to avoid GC issues with the boxed managed object reference. - w.Write(" public static WindowsRuntimeObjectReferenceValue BoxToUnmanaged("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - if (isEnum || almostBlittable || isComplexStruct) - { - w.Write("? value)\n {\n"); - w.Write(" return WindowsRuntimeValueTypeMarshaller.BoxToUnmanaged(value, CreateComInterfaceFlags."); - w.Write(hasReferenceFields ? "TrackerSupport" : "None"); - w.Write(", in "); - WriteIidReferenceExpression(w, type); - w.Write(");\n }\n"); - } - else - { - // Mapped struct (Duration/KeyTime/etc.): BoxToUnmanaged is still required because the - // public projected type still routes through this marshaller (it just lacks per-field - // ConvertToUnmanaged/ConvertToManaged because the field layout doesn't match). - w.Write("? value)\n {\n"); - w.Write(" return WindowsRuntimeValueTypeMarshaller.BoxToUnmanaged(value, CreateComInterfaceFlags.None, in "); - WriteIidReferenceExpression(w, type); - w.Write(");\n }\n"); - } - - // UnboxToManaged: simple for almost-blittable; for complex, unbox to ABI struct then ConvertToManaged. - w.Write(" public static "); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - if (isEnum || almostBlittable) - { - w.Write("? UnboxToManaged(void* value)\n {\n"); - w.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(">(value);\n }\n"); - } - else if (isComplexStruct) - { - w.Write("? UnboxToManaged(void* value)\n {\n"); - w.Write(" "); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write("? abi = WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write(">(value);\n"); - w.Write(" return abi.HasValue ? ConvertToManaged(abi.GetValueOrDefault()) : null;\n }\n"); - } - else - { - // Mapped struct: unbox directly to projected type (no per-field ConvertToManaged needed - // because the projected struct's field layout matches the WinMD struct layout). - w.Write("? UnboxToManaged(void* value)\n {\n"); - w.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(">(value);\n }\n"); - } - - w.Write("}\n\n"); - - // Emit the InterfaceEntriesImpl static class and the proper ComWrappersMarshallerAttribute - // class derived from WindowsRuntimeComWrappersMarshallerAttribute (matches truth). - // For enums and almost-blittable structs, GetOrCreateComInterfaceForObject uses None. - // For complex structs (with reference fields), it uses TrackerSupport. - // For complex structs, CreateObject converts via the *Marshaller.ConvertToManaged after - // unboxing to the ABI struct. - if (isEnum || almostBlittable || isComplexStruct) - { - string iidRefExpr = w.WriteTemp("%", new System.Action(_ => WriteIidReferenceExpression(w, type))); - - // InterfaceEntriesImpl - w.Write("file static class "); - w.Write(nameStripped); - w.Write("InterfaceEntriesImpl\n{\n"); - w.Write(" [FixedAddressValueType]\n"); - w.Write(" public static readonly ReferenceInterfaceEntries Entries;\n\n"); - w.Write(" static "); - w.Write(nameStripped); - w.Write("InterfaceEntriesImpl()\n {\n"); - w.Write(" Entries.IReferenceValue.IID = "); - w.Write(iidRefExpr); - w.Write(";\n"); - w.Write(" Entries.IReferenceValue.Vtable = "); - w.Write(nameStripped); - w.Write("ReferenceImpl.Vtable;\n"); - w.Write(" Entries.IPropertyValue.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IPropertyValue;\n"); - w.Write(" Entries.IPropertyValue.Vtable = global::WindowsRuntime.InteropServices.IPropertyValueImpl.OtherTypeVtable;\n"); - w.Write(" Entries.IStringable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable;\n"); - w.Write(" Entries.IStringable.Vtable = global::WindowsRuntime.InteropServices.IStringableImpl.Vtable;\n"); - w.Write(" Entries.IWeakReferenceSource.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IWeakReferenceSource;\n"); - w.Write(" Entries.IWeakReferenceSource.Vtable = global::WindowsRuntime.InteropServices.IWeakReferenceSourceImpl.Vtable;\n"); - w.Write(" Entries.IMarshal.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IMarshal;\n"); - w.Write(" Entries.IMarshal.Vtable = global::WindowsRuntime.InteropServices.IMarshalImpl.Vtable;\n"); - w.Write(" Entries.IAgileObject.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IAgileObject;\n"); - w.Write(" Entries.IAgileObject.Vtable = global::WindowsRuntime.InteropServices.IAgileObjectImpl.Vtable;\n"); - w.Write(" Entries.IInspectable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IInspectable;\n"); - w.Write(" Entries.IInspectable.Vtable = global::WindowsRuntime.InteropServices.IInspectableImpl.Vtable;\n"); - w.Write(" Entries.IUnknown.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IUnknown;\n"); - w.Write(" Entries.IUnknown.Vtable = global::WindowsRuntime.InteropServices.IUnknownImpl.Vtable;\n"); - w.Write(" }\n}\n\n"); - - // Mirror C++ write_abi_struct: in component mode the ComWrappers marshaller attribute - // is NOT emitted for STRUCTS (the attribute is supplied by cswinrtgen instead). Enums - // and other types still emit it from write_abi_enum/etc. - if (w.Settings.Component && cat == TypeCategory.Struct) { return; } - - // ComWrappersMarshallerAttribute (full body) - w.Write("internal sealed unsafe class "); - w.Write(nameStripped); - w.Write("ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute\n{\n"); - w.Write(" public override void* GetOrCreateComInterfaceForObject(object value)\n {\n"); - w.Write(" return WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags."); - w.Write(hasReferenceFields ? "TrackerSupport" : "None"); - w.Write(");\n }\n\n"); - w.Write(" public override ComInterfaceEntry* ComputeVtables(out int count)\n {\n"); - w.Write(" count = sizeof(ReferenceInterfaceEntries) / sizeof(ComInterfaceEntry);\n"); - w.Write(" return (ComInterfaceEntry*)Unsafe.AsPointer(in "); - w.Write(nameStripped); - w.Write("InterfaceEntriesImpl.Entries);\n }\n\n"); - w.Write(" public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" wrapperFlags = CreatedWrapperFlags.NonWrapping;\n"); - if (isComplexStruct) - { - w.Write(" return "); - w.Write(nameStripped); - w.Write("Marshaller.ConvertToManaged(WindowsRuntimeValueTypeMarshaller.UnboxToManagedUnsafe<"); - WriteTypedefName(w, type, TypedefNameType.ABI, true); - w.Write(">(value, in "); - w.Write(iidRefExpr); - w.Write("));\n"); - } - else - { - w.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManagedUnsafe<"); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(">(value, in "); - w.Write(iidRefExpr); - w.Write(");\n"); - } - w.Write(" }\n}\n"); - } - else - { - // Fallback: keep the placeholder class so consumer attribute references resolve. - w.Write("internal sealed class "); - w.Write(nameStripped); - w.Write("ComWrappersMarshallerAttribute : global::System.Attribute\n{\n}\n"); - } - } - - /// - /// Writes a marshaller stub for a delegate. - /// - /// - /// Emits just the <Name>Marshaller class for a delegate. Mirrors C++ - /// write_delegate_marshaller. - /// - private static void WriteDelegateMarshallerOnly(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string typeNs = type.Namespace?.Value ?? string.Empty; - string fullProjected = $"global::{typeNs}.{nameStripped}"; - string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); - - w.Write("\npublic static unsafe class "); - w.Write(nameStripped); - w.Write("Marshaller\n{\n"); - w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); - w.Write(fullProjected); - w.Write(" value)\n {\n"); - w.Write(" return WindowsRuntimeDelegateMarshaller.ConvertToUnmanaged(value, in "); - w.Write(iidExpr); - w.Write(");\n }\n\n"); - w.Write("#nullable enable\n"); - w.Write(" public static "); - w.Write(fullProjected); - w.Write("? ConvertToManaged(void* value)\n {\n"); - w.Write(" return ("); - w.Write(fullProjected); - w.Write("?)WindowsRuntimeDelegateMarshaller.ConvertToManaged<"); - w.Write(nameStripped); - w.Write("ComWrappersCallback>(value);\n }\n"); - w.Write("#nullable disable\n"); - w.Write("}\n"); - } - - /// - /// Emits the <Name>ComWrappersCallback file-scoped class for a delegate. - /// Mirrors C++ write_delegate_comwrappers_callback. Generic delegates are not emitted - /// here at all — the higher-level dispatch in ProjectionGenerator filters out generic - /// types from ABI emission (mirrors C++ main.cpp:412: - /// if (distance(type.GenericParam()) != 0) { continue; }). Open generic delegates - /// can't compile this body anyway because the projected type would have unbound generic - /// parameters. - /// - private static void WriteDelegateComWrappersCallback(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string typeNs = type.Namespace?.Value ?? string.Empty; - string fullProjected = $"global::{typeNs}.{nameStripped}"; - string iidExpr = w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, type))); - - MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); - bool nativeSupported = invoke is not null && IsDelegateInvokeNativeSupported(new MethodSig(invoke)); - - w.Write("\nfile abstract unsafe class "); - w.Write(nameStripped); - w.Write("ComWrappersCallback : IWindowsRuntimeObjectComWrappersCallback\n{\n"); - w.Write(" /// \n"); - w.Write(" public static object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); - w.Write(" externalComObject: value,\n"); - w.Write(" iid: in "); - w.Write(iidExpr); - w.Write(",\n wrapperFlags: out wrapperFlags);\n\n"); - // Always emit the body. The 'valueReference.Invoke' extension method always - // exists (in NativeDelegate); even when its body is itself a stub, this path compiles - // and matches the truth, which never emits 'throw null!' for CreateObject. - w.Write(" return new "); - w.Write(fullProjected); - w.Write("(valueReference."); - w.Write(nameStripped); - w.Write("Invoke);\n"); - _ = nativeSupported; - w.Write(" }\n}\n"); - } - - /// - /// Emits the <Name>ComWrappersMarshallerAttribute class. Mirrors C++ - /// write_delegate_com_wrappers_marshaller_attribute_impl. Generic delegates are not - /// emitted here at all (filtered out in ProjectionGenerator). - /// - private static void WriteDelegateComWrappersMarshallerAttribute(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string iidRefExpr = w.WriteTemp("%", new System.Action(_ => WriteIidReferenceExpression(w, type))); - - w.Write("\ninternal sealed unsafe class "); - w.Write(nameStripped); - w.Write("ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute\n{\n"); - w.Write(" /// \n"); - w.Write(" public override void* GetOrCreateComInterfaceForObject(object value)\n {\n"); - w.Write(" return WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.TrackerSupport);\n"); - w.Write(" }\n\n"); - w.Write(" /// \n"); - w.Write(" public override ComInterfaceEntry* ComputeVtables(out int count)\n {\n"); - w.Write(" count = sizeof(DelegateReferenceInterfaceEntries) / sizeof(ComInterfaceEntry);\n\n"); - w.Write(" return (ComInterfaceEntry*)Unsafe.AsPointer(in "); - w.Write(nameStripped); - w.Write("InterfaceEntriesImpl.Entries);\n }\n\n"); - w.Write(" /// \n"); - w.Write(" public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" wrapperFlags = CreatedWrapperFlags.NonWrapping;\n"); - w.Write(" return WindowsRuntimeDelegateMarshaller.UnboxToManaged<"); - w.Write(nameStripped); - w.Write("ComWrappersCallback>(value, in "); - w.Write(iidRefExpr); - w.Write(")!;\n }\n"); - w.Write("}\n"); - } - - /// True if EmitNativeDelegateBody can emit a real (non-throw) body for this signature. - private static bool IsDelegateInvokeNativeSupported(MethodSig sig) - { - AsmResolver.DotNet.Signatures.TypeSignature? rt = sig.ReturnType; - if (rt is not null) - { - if (IsHResultException(rt)) { return false; } - if (!(IsBlittablePrimitive(rt) || IsAnyStruct(rt) || IsString(rt) || IsRuntimeClassOrInterface(rt) || IsObject(rt) || IsGenericInstance(rt) || IsComplexStruct(rt))) { return false; } - } - foreach (ParamInfo p in sig.Params) - { - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - { - if (p.Type is AsmResolver.DotNet.Signatures.SzArrayTypeSignature szP) - { - if (IsBlittablePrimitive(szP.BaseType)) { continue; } - if (IsAnyStruct(szP.BaseType)) { continue; } - } - return false; - } - if (cat != ParamCategory.In) { return false; } - if (IsHResultException(p.Type)) { return false; } - if (IsBlittablePrimitive(p.Type)) { continue; } - if (IsAnyStruct(p.Type)) { continue; } - if (IsString(p.Type)) { continue; } - if (IsRuntimeClassOrInterface(p.Type)) { continue; } - if (IsObject(p.Type)) { continue; } - if (IsGenericInstance(p.Type)) { continue; } - if (IsComplexStruct(p.Type)) { continue; } - return false; - } - return true; - } - - /// - /// Writes the marshaller infrastructure for a runtime class: - /// * Public *Marshaller class with real ConvertToUnmanaged/ConvertToManaged bodies - /// * file-scoped *ComWrappersMarshallerAttribute (CreateObject implementation) - /// * file-scoped *ComWrappersCallback (IWindowsRuntimeObjectComWrappersCallback for sealed, - /// IWindowsRuntimeUnsealedObjectComWrappersCallback for unsealed) - /// Mirrors C++ write_class_marshaller, write_class_comwrappers_marshaller_attribute, - /// and write_class_comwrappers_callback. - /// - private static void WriteClassMarshallerStub(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string typeNs = type.Namespace?.Value ?? string.Empty; - string fullProjected = $"global::{typeNs}.{nameStripped}"; - - // Get the IID expression for the default interface (used by CreateObject). - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); - string defaultIfaceIid = defaultIface is not null - ? w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, defaultIface))) - : "default(global::System.Guid)"; - - // Determine the marshalingType expression from the class's [MarshalingBehaviorAttribute] - // (mirrors C++ get_marshaling_type_name). This is used by both the marshaller attribute and the - // callback (the C++ code uses the same value for both). - string marshalingType = GetMarshalingTypeName(type); - - bool isSealed = type.IsSealed; - - // For unsealed classes, the ConvertToUnmanaged path needs to know whether the default interface is - // exclusive-to (mirrors C++ logic). - TypeDefinition? defaultIfaceTd = defaultIface is null ? null : ResolveInterfaceTypeDef(defaultIface); - bool defaultIfaceIsExclusive = defaultIfaceTd is not null && TypeCategorization.IsExclusiveTo(defaultIfaceTd); - - // Public *Marshaller class - w.Write("public static unsafe class "); - w.Write(nameStripped); - w.Write("Marshaller\n{\n"); - w.Write(" public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged("); - w.Write(fullProjected); - w.Write(" value)\n {\n"); - if (isSealed) - { - // For projected sealed runtime classes, the RCW type is always unwrappable. - w.Write(" if (value is not null)\n {\n"); - w.Write(" return WindowsRuntimeComWrappersMarshal.UnwrapObjectReferenceUnsafe(value).AsValue();\n"); - w.Write(" }\n"); - } - else if (!defaultIfaceIsExclusive && defaultIface is not null) - { - string defIfaceTypeName = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, TypeSemanticsFactory.Get(defaultIface.ToTypeSignature(false)), TypedefNameType.Projected, false))); - w.Write(" if (value is IWindowsRuntimeInterface<"); - w.Write(defIfaceTypeName); - w.Write("> windowsRuntimeInterface)\n {\n"); - w.Write(" return windowsRuntimeInterface.GetInterface();\n"); - w.Write(" }\n"); - } - else - { - w.Write(" if (value is not null)\n {\n"); - w.Write(" return value.GetDefaultInterface();\n"); - w.Write(" }\n"); - } - w.Write(" return default;\n }\n\n"); - w.Write(" public static "); - w.Write(fullProjected); - w.Write("? ConvertToManaged(void* value)\n {\n"); - w.Write(" return ("); - w.Write(fullProjected); - w.Write("?)"); - w.Write(isSealed ? "WindowsRuntimeObjectMarshaller" : "WindowsRuntimeUnsealedObjectMarshaller"); - w.Write(".ConvertToManaged<"); - w.Write(nameStripped); - w.Write("ComWrappersCallback>(value);\n }\n}\n\n"); - - // file-scoped *ComWrappersMarshallerAttribute - implements WindowsRuntimeComWrappersMarshallerAttribute.CreateObject - w.Write("file sealed unsafe class "); - w.Write(nameStripped); - w.Write("ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute\n{\n"); - EmitUnsafeAccessorForDefaultIfaceIfGeneric(w, defaultIface); - w.Write(" public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReference(\n"); - w.Write(" externalComObject: value,\n"); - w.Write(" iid: "); - w.Write(defaultIfaceIid); - w.Write(",\n"); - w.Write(" marshalingType: "); - w.Write(marshalingType); - w.Write(",\n"); - w.Write(" wrapperFlags: out wrapperFlags);\n\n"); - w.Write(" return new "); - w.Write(fullProjected); - w.Write("(valueReference);\n }\n}\n\n"); - - if (isSealed) - { - // file-scoped *ComWrappersCallback - implements IWindowsRuntimeObjectComWrappersCallback - w.Write("file sealed unsafe class "); - w.Write(nameStripped); - w.Write("ComWrappersCallback : IWindowsRuntimeObjectComWrappersCallback\n{\n"); - EmitUnsafeAccessorForDefaultIfaceIfGeneric(w, defaultIface); - w.Write(" public static object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); - w.Write(" externalComObject: value,\n"); - w.Write(" iid: "); - w.Write(defaultIfaceIid); - w.Write(",\n"); - w.Write(" marshalingType: "); - w.Write(marshalingType); - w.Write(",\n"); - w.Write(" wrapperFlags: out wrapperFlags);\n\n"); - w.Write(" return new "); - w.Write(fullProjected); - w.Write("(valueReference);\n }\n}\n"); - } - else - { - // file-scoped *ComWrappersCallback - implements IWindowsRuntimeUnsealedObjectComWrappersCallback - string nonProjectedRcn = $"{typeNs}.{nameStripped}"; - w.Write("file sealed unsafe class "); - w.Write(nameStripped); - w.Write("ComWrappersCallback : IWindowsRuntimeUnsealedObjectComWrappersCallback\n{\n"); - EmitUnsafeAccessorForDefaultIfaceIfGeneric(w, defaultIface); - - // TryCreateObject (non-projected runtime class name match) - w.Write(" public static unsafe bool TryCreateObject(\n"); - w.Write(" void* value,\n"); - w.Write(" ReadOnlySpan runtimeClassName,\n"); - w.Write(" [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? wrapperObject,\n"); - w.Write(" out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" if (runtimeClassName.SequenceEqual(\""); - w.Write(nonProjectedRcn); - w.Write("\".AsSpan()))\n {\n"); - w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); - w.Write(" externalComObject: value,\n"); - w.Write(" iid: "); - w.Write(defaultIfaceIid); - w.Write(",\n"); - w.Write(" marshalingType: "); - w.Write(marshalingType); - w.Write(",\n"); - w.Write(" wrapperFlags: out wrapperFlags);\n\n"); - w.Write(" wrapperObject = new "); - w.Write(fullProjected); - w.Write("(valueReference);\n return true;\n }\n\n"); - w.Write(" wrapperObject = null;\n wrapperFlags = CreatedWrapperFlags.None;\n return false;\n }\n\n"); - - // CreateObject (fallback) - w.Write(" public static unsafe object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)\n {\n"); - w.Write(" WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe(\n"); - w.Write(" externalComObject: value,\n"); - w.Write(" iid: "); - w.Write(defaultIfaceIid); - w.Write(",\n"); - w.Write(" marshalingType: "); - w.Write(marshalingType); - w.Write(",\n"); - w.Write(" wrapperFlags: out wrapperFlags);\n\n"); - w.Write(" return new "); - w.Write(fullProjected); - w.Write("(valueReference);\n }\n}\n"); - } - } - - /// - /// Emits the [UnsafeAccessor] declaration for the default interface IID inside a file-scoped - /// ComWrappers class. Only emits if the default interface is a generic instantiation. - /// Mirrors C++ write_class_comwrappers_marshaller_attribute / write_class_comwrappers_callback - /// behavior of inserting write_unsafe_accessor_for_iid at the top of the class body. - /// - private static void EmitUnsafeAccessorForDefaultIfaceIfGeneric(TypeWriter w, ITypeDefOrRef? defaultIface) - { - if (defaultIface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) - { - EmitUnsafeAccessorForIid(w, gi); - } - } - - /// - /// Writes a minimal interface 'Methods' static class with method body emission. - /// Mirrors C++ write_static_abi_methods: void/no-args methods and - /// blittable-primitive-return/no-args methods get real implementations; everything else - /// remains as 'throw null!' stubs (deferred — needs full per-parameter marshalling). - /// - private static void WriteInterfaceMarshallerStub(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - // Mirrors C++ write_static_abi_classes: visibility is internal if the interface is - // exclusive to a class (and not opted into PublicExclusiveTo) or if it's marked - // [ProjectionInternal]; public otherwise. - bool useInternal = (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) - || TypeCategorization.IsProjectionInternal(type); - - // Fast ABI: if this interface is a non-default exclusive-to interface of a fast-abi - // class, skip emitting it entirely — its members are merged into the default - // interface's Methods class. Mirrors C++ code_writers.h:9082-9089 - // (write_static_abi_classes early return on contains_other_interface(iface)). - if (IsFastAbiOtherInterface(type)) { return; } - - // If the interface is exclusive-to a class that's been excluded from the projection, - // skip emitting the entire *Methods class — it would be dead code (the owning class - // is manually projected in WinRT.Runtime, e.g. IColorHelperStatics for ColorHelper, - // IColorsStatics for Colors, IFontWeightsStatics for FontWeights). The C++ tool also - // omits these because their owning class is not projected. - if (TypeCategorization.IsExclusiveTo(type)) - { - TypeDefinition? owningClass = GetExclusiveToType(type); - if (owningClass is not null && !w.Settings.Filter.Includes(owningClass)) - { - return; - } - } - - // Mirrors C++ skip_exclusive_events: events on exclusive interfaces (used by the class) - // are inlined in the RCW class, so we skip emitting them in the Methods type. - bool skipExclusiveEvents = false; - if (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) - { - TypeDefinition? classType = GetExclusiveToType(type); - if (classType is not null) - { - foreach (InterfaceImplementation impl in classType.Interfaces) - { - TypeDefinition? implDef = ResolveInterfaceTypeDef(impl.Interface!); - if (implDef is not null && implDef == type) - { - skipExclusiveEvents = true; - break; - } - } - } - } - - // Fast ABI: if this interface is the default interface of a fast-abi class, the - // generated Methods class must include the merged members of the default interface - // PLUS each [ExclusiveTo] non-default interface in vtable order, with progressively - // increasing slot indices. Mirrors C++ code_writers.h:9113-9128. - // For non-fast-abi interfaces, the segment list is just [(type, INSPECTABLE_METHOD_COUNT, skipExclusiveEvents)]. - const int InspectableMethodCount = 6; - var segments = new List<(TypeDefinition Iface, int StartSlot, bool SkipEvents)>(); - var fastAbi = GetFastAbiClassForInterface(type); - bool isFastAbiDefault = fastAbi is not null && fastAbi.Value.Default is not null - && InterfacesEqualByName(fastAbi.Value.Default, type); - if (isFastAbiDefault) - { - int slot = InspectableMethodCount; - // Default interface: skip its events (they're inlined in the RCW class). - segments.Add((type, slot, true)); - slot += CountMethods(type) + GetClassHierarchyIndex(fastAbi!.Value.Class); - foreach (TypeDefinition other in fastAbi.Value.Others) - { - segments.Add((other, slot, false)); - slot += CountMethods(other); - } - } - else - { - segments.Add((type, InspectableMethodCount, skipExclusiveEvents)); - } - - // Skip emission if the entire merged class would be empty. - bool hasAnyMember = false; - foreach ((TypeDefinition seg, int _, bool segSkipEvents) in segments) - { - if (HasEmittableMembers(seg, segSkipEvents)) { hasAnyMember = true; break; } - } - if (!hasAnyMember) { return; } - - w.Write(useInternal ? "internal static class " : "public static class "); - w.Write(nameStripped); - w.Write("Methods\n{\n"); - - foreach ((TypeDefinition iface, int startSlot, bool segSkipEvents) in segments) - { - EmitMethodsClassMembersFor(w, iface, startSlot, segSkipEvents); - } - - w.Write("}\n"); - } - - /// True if the interface has at least one non-special method, property, or non-skipped event. - private static bool HasEmittableMembers(TypeDefinition iface, bool skipExclusiveEvents) - { - foreach (MethodDefinition m in iface.Methods) - { - if (!Helpers.IsSpecial(m)) { return true; } - } - foreach (PropertyDefinition _ in iface.Properties) { return true; } - if (!skipExclusiveEvents) - { - foreach (EventDefinition _ in iface.Events) { return true; } - } - return false; - } - - /// Returns the number of methods (including special accessors) on the interface. - private static int CountMethods(TypeDefinition iface) - { - int count = 0; - foreach (MethodDefinition _ in iface.Methods) { count++; } - return count; - } - - /// Mirrors C++ get_class_hierarchy_index: distance from in inheritance. - private static int GetClassHierarchyIndex(TypeDefinition classType) - { - if (classType.BaseType is null) { return 0; } - string ns = classType.BaseType.Namespace?.Value ?? string.Empty; - string nm = classType.BaseType.Name?.Value ?? string.Empty; - if (ns == "System" && nm == "Object") { return 0; } - TypeDefinition? baseDef = classType.BaseType as TypeDefinition; - if (baseDef is null && _cacheRef is not null) - { - try { baseDef = classType.BaseType.Resolve(_cacheRef.RuntimeContext); } - catch { baseDef = null; } - baseDef ??= _cacheRef.Find(string.IsNullOrEmpty(ns) ? nm : (ns + "." + nm)); - } - if (baseDef is null) { return 0; } - return GetClassHierarchyIndex(baseDef) + 1; - } - - private static bool InterfacesEqualByName(TypeDefinition a, TypeDefinition b) - { - if (a == b) { return true; } - return (a.Namespace?.Value ?? string.Empty) == (b.Namespace?.Value ?? string.Empty) - && (a.Name?.Value ?? string.Empty) == (b.Name?.Value ?? string.Empty); - } - - /// - /// Emits the per-interface members (methods, properties, events) into an already-open Methods - /// static class. Used both for the standalone case and for the fast-abi merged emission. - /// - private static void EmitMethodsClassMembersFor(TypeWriter w, TypeDefinition type, int startSlot, bool skipExclusiveEvents) - { - // Build a map from each MethodDefinition to its WinMD vtable slot. - // Mirrors C++ get_vmethod_index: slot = (method.index() - vtable_base) + start_slot. - // In AsmResolver, type.Methods is iterated in MethodDef row order, so the position of each - // method in type.Methods (relative to the first method of the type) gives us the same value. - Dictionary methodSlot = new(); - { - int idx = 0; - foreach (MethodDefinition m in type.Methods) - { - methodSlot[m] = idx + startSlot; - idx++; - } - } - - // Emit non-special methods first (output order is unchanged from before; only the slot lookup changes). - foreach (MethodDefinition method in type.Methods) - { - if (Helpers.IsSpecial(method)) { continue; } - string mname = method.Name?.Value ?? string.Empty; - MethodSig sig = new(method); - - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" public static unsafe "); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(mname); - w.Write("(WindowsRuntimeObjectReference thisReference"); - if (sig.Params.Count > 0) { w.Write(", "); } - WriteParameterList(w, sig); - w.Write(")"); - - // Emit the body if we can handle this case. Slot comes from the method's WinMD index. - EmitAbiMethodBodyIfSimple(w, sig, methodSlot[method], isNoExcept: Helpers.IsNoExcept(method)); - } - - // Emit property accessors. Each getter / setter consumes one vtable slot — looked up from the underlying method. - foreach (PropertyDefinition prop in type.Properties) - { - string pname = prop.Name?.Value ?? string.Empty; - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - string propType = WritePropType(w, prop); - (MethodDefinition? gMethod, MethodDefinition? sMethod) = (getter, setter); - // Mirrors C++ helpers.h:46-49: the [NoException] check on properties applies to BOTH - // accessors of the property (the attribute is on the property itself, not on the - // individual accessors). - bool propIsNoExcept = Helpers.IsNoExcept(prop); - if (gMethod is not null) - { - MethodSig getSig = new(gMethod); - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" public static unsafe "); - w.Write(propType); - w.Write(" "); - w.Write(pname); - w.Write("(WindowsRuntimeObjectReference thisReference)"); - EmitAbiMethodBodyIfSimple(w, getSig, methodSlot[gMethod], isNoExcept: propIsNoExcept); - } - if (sMethod is not null) - { - MethodSig setSig = new(sMethod); - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" public static unsafe void "); - w.Write(pname); - w.Write("(WindowsRuntimeObjectReference thisReference, "); - // Mirrors C++ code_writers.h:7193 — setter parameter uses the is_set_property=true - // form of write_prop_type, which for SZ array types emits ReadOnlySpan instead - // of T[] (the getter's return-type form). - w.Write(WritePropType(w, prop, isSetProperty: true)); - w.Write(" value)"); - EmitAbiMethodBodyIfSimple(w, setSig, methodSlot[sMethod], paramNameOverride: "value", isNoExcept: propIsNoExcept); - } - } - - // Emit event member methods (returns an event source, takes thisObject + thisReference). - // Skip events on exclusive interfaces used by their class — they're inlined directly in - // the RCW class. (Mirrors C++ skip_exclusive_events.) - foreach (EventDefinition evt in type.Events) - { - if (skipExclusiveEvents) { continue; } - string evtName = evt.Name?.Value ?? string.Empty; - AsmResolver.DotNet.Signatures.TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); - bool isGenericEvent = evtSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; - - // Use the add method's WinMD slot. Mirrors C++: events use the add_X method's vmethod_index. - (MethodDefinition? addMethod, MethodDefinition? _) = Helpers.GetEventMethods(evt); - int eventSlot = addMethod is not null && methodSlot.TryGetValue(addMethod, out int es) ? es : 0; - - // Build the projected event source type name. For non-generic delegate handlers, the - // EventSource subclass lives in the ABI namespace alongside this Methods class, so - // we need to use the ABI-qualified name. For generic handlers (Windows.Foundation.*EventHandler), - // it's mapped to global::WindowsRuntime.InteropServices.EventHandlerEventSource<...>. - string eventSourceProjectedFull; - if (isGenericEvent) - { - eventSourceProjectedFull = w.WriteTemp("%", new System.Action(_ => - WriteTypeName(w, TypeSemanticsFactory.Get(evtSig), TypedefNameType.EventSource, true))); - if (!eventSourceProjectedFull.StartsWith("global::", System.StringComparison.Ordinal)) - { - eventSourceProjectedFull = "global::" + eventSourceProjectedFull; - } - } - else - { - // Non-generic delegate handler: the EventSource lives in the same ABI namespace - // as this Methods class, so we use just the short name (matches truth output). - string delegateName = string.Empty; - if (evtSig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - delegateName = td.Type?.Name?.Value ?? string.Empty; - delegateName = Helpers.StripBackticks(delegateName); - } - eventSourceProjectedFull = delegateName + "EventSource"; - } - string eventSourceInteropType = isGenericEvent - ? EncodeInteropTypeName(evtSig, TypedefNameType.EventSource) + ", WinRT.Interop" - : string.Empty; - - // Emit the per-event ConditionalWeakTable static field. - w.Write("\n private static ConditionalWeakTable _"); - w.Write(evtName); - w.Write("\n {\n"); - w.Write(" [MethodImpl(MethodImplOptions.AggressiveInlining)]\n"); - w.Write(" get\n {\n"); - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" static ConditionalWeakTable MakeTable()\n {\n"); - w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(ref field, [], null);\n\n"); - w.Write(" return global::System.Threading.Volatile.Read(in field);\n"); - w.Write(" }\n\n"); - w.Write(" return global::System.Threading.Volatile.Read(in field) ?? MakeTable();\n }\n }\n"); - - // Emit the static method that returns the per-instance event source. - w.Write("\n public static "); - w.Write(eventSourceProjectedFull); - w.Write(" "); - w.Write(evtName); - w.Write("(object thisObject, WindowsRuntimeObjectReference thisReference)\n {\n"); - if (isGenericEvent && !string.IsNullOrEmpty(eventSourceInteropType)) - { - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.Constructor)]\n"); - w.Write(" [return: UnsafeAccessorType(\""); - w.Write(eventSourceInteropType); - w.Write("\")]\n"); - w.Write(" static extern object ctor(WindowsRuntimeObjectReference nativeObjectReference, int index);\n\n"); - w.Write(" return _"); - w.Write(evtName); - w.Write(".GetOrAdd(\n"); - w.Write(" key: thisObject,\n"); - w.Write(" valueFactory: static (_, thisReference) => Unsafe.As<"); - w.Write(eventSourceProjectedFull); - w.Write(">(ctor(thisReference, "); - w.Write(eventSlot.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(")),\n"); - w.Write(" factoryArgument: thisReference);\n"); - } - else - { - // Non-generic delegate: directly construct. - w.Write(" return _"); - w.Write(evtName); - w.Write(".GetOrAdd(\n"); - w.Write(" key: thisObject,\n"); - w.Write(" valueFactory: static (_, thisReference) => new "); - w.Write(eventSourceProjectedFull); - w.Write("(thisReference, "); - w.Write(eventSlot.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write("),\n"); - w.Write(" factoryArgument: thisReference);\n"); - } - w.Write(" }\n"); - } - } - - /// - /// Emits a real method body for the cases we can fully marshal, otherwise emits - /// the 'throw null!' stub. Trailing newline is included. - /// - /// When true, the vtable call is emitted WITHOUT the - /// RestrictedErrorInfo.ThrowExceptionForHR(...) wrap. Mirrors C++ - /// code_writers.h:6725 which checks has_noexcept_attr - /// (is_noexcept(MethodDef) / is_noexcept(Property) in helpers.h:41-49): - /// methods/properties annotated with [Windows.Foundation.Metadata.NoExceptionAttribute] - /// (or remove-overload methods) contractually return S_OK, so the wrap is omitted. - private static void EmitAbiMethodBodyIfSimple(TypeWriter w, MethodSig sig, int slot, string? paramNameOverride = null, bool isNoExcept = false) - { - AsmResolver.DotNet.Signatures.TypeSignature? rt = sig.ReturnType; - - bool returnIsString = rt is not null && IsString(rt); - bool returnIsRefType = rt is not null && (IsRuntimeClassOrInterface(rt) || IsObject(rt) || IsGenericInstance(rt)); - bool returnIsAnyStruct = rt is not null && IsAnyStruct(rt); - bool returnIsComplexStruct = rt is not null && IsComplexStruct(rt); - bool returnIsReceiveArray = rt is AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSzCheck - && (IsBlittablePrimitive(retSzCheck.BaseType) || IsAnyStruct(retSzCheck.BaseType) - || IsString(retSzCheck.BaseType) || IsRuntimeClassOrInterface(retSzCheck.BaseType) || IsObject(retSzCheck.BaseType) - || IsComplexStruct(retSzCheck.BaseType) - || IsHResultException(retSzCheck.BaseType) - || IsMappedAbiValueType(retSzCheck.BaseType)); - bool returnIsHResultException = rt is not null && IsHResultException(rt); - - // Build the function pointer signature: void*, [paramAbiType...,] [retAbiType*,] int - System.Text.StringBuilder fp = new(); - fp.Append("void*"); - foreach (ParamInfo p in sig.Params) - { - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - { - fp.Append(", uint, void*"); - continue; - } - if (cat == ParamCategory.Out) - { - AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); - fp.Append(", "); - if (IsString(uOut) || IsRuntimeClassOrInterface(uOut) || IsObject(uOut) || IsGenericInstance(uOut)) { fp.Append("void**"); } - else if (IsSystemType(uOut)) { fp.Append("global::ABI.System.Type*"); } - else if (IsComplexStruct(uOut)) { fp.Append(GetAbiStructTypeName(w, uOut)); fp.Append('*'); } - else if (IsAnyStruct(uOut)) { fp.Append(GetBlittableStructAbiType(w, uOut)); fp.Append('*'); } - else { fp.Append(GetAbiPrimitiveType(uOut)); fp.Append('*'); } - continue; - } - if (cat == ParamCategory.Ref) - { - AsmResolver.DotNet.Signatures.TypeSignature uRef = StripByRefAndCustomModifiers(p.Type); - fp.Append(", "); - if (IsComplexStruct(uRef)) { fp.Append(GetAbiStructTypeName(w, uRef)); fp.Append('*'); } - else if (IsAnyStruct(uRef)) { fp.Append(GetBlittableStructAbiType(w, uRef)); fp.Append('*'); } - else { fp.Append(GetAbiPrimitiveType(uRef)); fp.Append('*'); } - continue; - } - if (cat == ParamCategory.ReceiveArray) - { - AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); - fp.Append(", uint*, "); - if (IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType)) - { - fp.Append("void*"); - } - else if (IsHResultException(sza.BaseType)) - { - fp.Append("global::ABI.System.Exception"); - } - else if (IsMappedAbiValueType(sza.BaseType)) - { - fp.Append(GetMappedAbiTypeName(sza.BaseType)); - } - else if (IsComplexStruct(sza.BaseType)) { fp.Append(GetAbiStructTypeName(w, sza.BaseType)); } - else if (IsAnyStruct(sza.BaseType)) { fp.Append(GetBlittableStructAbiType(w, sza.BaseType)); } - else { fp.Append(GetAbiPrimitiveType(sza.BaseType)); } - fp.Append("**"); - continue; - } - fp.Append(", "); - if (IsHResultException(p.Type)) { fp.Append("global::ABI.System.Exception"); } - else if (IsString(p.Type) || IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type) || IsGenericInstance(p.Type)) { fp.Append("void*"); } - else if (IsSystemType(p.Type)) { fp.Append("global::ABI.System.Type"); } - else if (IsAnyStruct(p.Type)) { fp.Append(GetBlittableStructAbiType(w, p.Type)); } - else if (IsMappedAbiValueType(p.Type)) { fp.Append(GetMappedAbiTypeName(p.Type)); } - else if (IsComplexStruct(p.Type)) { fp.Append(GetAbiStructTypeName(w, p.Type)); } - else { fp.Append(GetAbiPrimitiveType(p.Type)); } - } - if (rt is not null) - { - if (returnIsReceiveArray) - { - AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt; - fp.Append(", uint*, "); - if (IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType)) - { - fp.Append("void*"); - } - else if (IsComplexStruct(retSz.BaseType)) - { - fp.Append(GetAbiStructTypeName(w, retSz.BaseType)); - } - else if (IsHResultException(retSz.BaseType)) - { - fp.Append("global::ABI.System.Exception"); - } - else if (IsMappedAbiValueType(retSz.BaseType)) - { - fp.Append(GetMappedAbiTypeName(retSz.BaseType)); - } - else if (IsAnyStruct(retSz.BaseType)) - { - fp.Append(GetBlittableStructAbiType(w, retSz.BaseType)); - } - else - { - fp.Append(GetAbiPrimitiveType(retSz.BaseType)); - } - fp.Append("**"); - } - else if (returnIsHResultException) - { - fp.Append(", global::ABI.System.Exception*"); - } - else - { - fp.Append(", "); - if (returnIsString || returnIsRefType) { fp.Append("void**"); } - else if (rt is not null && IsSystemType(rt)) { fp.Append("global::ABI.System.Type*"); } - else if (returnIsAnyStruct) { fp.Append(GetBlittableStructAbiType(w, rt!)); fp.Append('*'); } - else if (returnIsComplexStruct) { fp.Append(GetAbiStructTypeName(w, rt!)); fp.Append('*'); } - else if (rt is not null && IsMappedAbiValueType(rt)) { fp.Append(GetMappedAbiTypeName(rt)); fp.Append('*'); } - else { fp.Append(GetAbiPrimitiveType(rt!)); fp.Append('*'); } - } - } - fp.Append(", int"); - - w.Write("\n {\n"); - w.Write(" using WindowsRuntimeObjectReferenceValue thisValue = thisReference.AsValue();\n"); - w.Write(" void* ThisPtr = thisValue.GetThisPtrUnsafe();\n"); - - // Declare 'using' marshaller values for ref-type parameters (these need disposing). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type)) - { - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - w.Write(" using WindowsRuntimeObjectReferenceValue __"); - w.Write(localName); - w.Write(" = "); - EmitMarshallerConvertToUnmanaged(w, p.Type, callName); - w.Write(";\n"); - } - else if (IsNullableT(p.Type)) - { - // Nullable param: use Marshaller.BoxToUnmanaged. Mirrors truth pattern. - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(p.Type)!; - string innerMarshaller = GetNullableInnerMarshallerName(w, inner); - w.Write(" using WindowsRuntimeObjectReferenceValue __"); - w.Write(localName); - w.Write(" = "); - w.Write(innerMarshaller); - w.Write(".BoxToUnmanaged("); - w.Write(callName); - w.Write(");\n"); - } - else if (IsGenericInstance(p.Type)) - { - // Generic instance param: emit a local UnsafeAccessor delegate to get the marshaller method. - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - string interopTypeName = EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, p.Type, false))); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); - w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, "); - w.Write(projectedTypeName); - w.Write(" value);\n"); - w.Write(" using WindowsRuntimeObjectReferenceValue __"); - w.Write(localName); - w.Write(" = ConvertToUnmanaged_"); - w.Write(localName); - w.Write("(null, "); - w.Write(callName); - w.Write(");\n"); - } - } - // (String input params are now stack-allocated via the fast-path pinning pattern below; - // no separate void* local declaration or up-front allocation is needed.) - // Declare locals for HResult/Exception input parameters (converted up-front). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - if (ParamHelpers.GetParamCategory(p) != ParamCategory.In) { continue; } - if (!IsHResultException(p.Type)) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - w.Write(" global::ABI.System.Exception __"); - w.Write(localName); - w.Write(" = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged("); - w.Write(callName); - w.Write(");\n"); - } - // Declare locals for mapped value-type input parameters (DateTime/TimeSpan): convert via marshaller up-front. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - if (ParamHelpers.GetParamCategory(p) != ParamCategory.In) { continue; } - if (!IsMappedAbiValueType(p.Type)) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - w.Write(" "); - w.Write(GetMappedAbiTypeName(p.Type)); - w.Write(" __"); - w.Write(localName); - w.Write(" = "); - w.Write(GetMappedMarshallerName(p.Type)); - w.Write(".ConvertToUnmanaged("); - w.Write(callName); - w.Write(");\n"); - } - // Declare locals for complex-struct input parameters (e.g. ProfileUsage with nested - // string/Nullable fields): default-initialize OUTSIDE try, assign inside try via marshaller, - // dispose in finally. Mirrors C++ behavior for non-blittable struct input params. - // Includes both 'in' (ParamCategory.In) and 'in T' (ParamCategory.Ref) forms. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.In && cat != ParamCategory.Ref) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature pType = StripByRefAndCustomModifiers(p.Type); - if (!IsComplexStruct(pType)) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - w.Write(" "); - w.Write(GetAbiStructTypeName(w, pType)); - w.Write(" __"); - w.Write(localName); - w.Write(" = default;\n"); - } - // Declare locals for Out parameters (need to be passed as &__ to the call). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); - w.Write(" "); - if (IsString(uOut) || IsRuntimeClassOrInterface(uOut) || IsObject(uOut) || IsGenericInstance(uOut)) { w.Write("void*"); } - else if (IsSystemType(uOut)) { w.Write("global::ABI.System.Type"); } - else if (IsComplexStruct(uOut)) { w.Write(GetAbiStructTypeName(w, uOut)); } - else if (IsAnyStruct(uOut)) { w.Write(GetBlittableStructAbiType(w, uOut)); } - else { w.Write(GetAbiPrimitiveType(uOut)); } - w.Write(" __"); - w.Write(localName); - w.Write(" = default;\n"); - } - // Declare locals for ReceiveArray params (uint length + element pointer). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.ReceiveArray) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); - w.Write(" uint __"); - w.Write(localName); - w.Write("_length = default;\n"); - w.Write(" "); - // Element ABI type: void* for ref types; ABI struct for complex/blittable structs; - // primitive ABI otherwise. - if (IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType)) - { - w.Write("void*"); - } - else if (IsComplexStruct(sza.BaseType)) - { - w.Write(GetAbiStructTypeName(w, sza.BaseType)); - } - else if (IsAnyStruct(sza.BaseType)) - { - w.Write(GetBlittableStructAbiType(w, sza.BaseType)); - } - else - { - w.Write(GetAbiPrimitiveType(sza.BaseType)); - } - w.Write("* __"); - w.Write(localName); - w.Write("_data = default;\n"); - } - // Declare InlineArray16 + ArrayPool fallback for non-blittable PassArray params - // (runtime classes, objects, strings). Runtime class/object: just one InlineArray16. - // String: also needs InlineArray16 + InlineArray16 for pinned handles. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - // Non-blittable element type: emit InlineArray16 + ArrayPool. - // For mapped value types (DateTime/TimeSpan), use the ABI struct type. - // For complex structs (e.g. authored BasicStruct with reference fields), use the ABI - // struct type. For everything else (runtime classes, objects, strings), use nint. - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - string storageT = IsMappedAbiValueType(szArr.BaseType) - ? GetMappedAbiTypeName(szArr.BaseType) - : IsComplexStruct(szArr.BaseType) - ? GetAbiStructTypeName(w, szArr.BaseType) - : IsHResultException(szArr.BaseType) - ? "global::ABI.System.Exception" - : "nint"; - w.Write("\n Unsafe.SkipInit(out InlineArray16<"); - w.Write(storageT); - w.Write("> __"); - w.Write(localName); - w.Write("_inlineArray);\n"); - w.Write(" "); - w.Write(storageT); - w.Write("[] __"); - w.Write(localName); - w.Write("_arrayFromPool = null;\n"); - w.Write(" Span<"); - w.Write(storageT); - w.Write("> __"); - w.Write(localName); - w.Write("_span = "); - w.Write(callName); - w.Write(".Length <= 16\n ? __"); - w.Write(localName); - w.Write("_inlineArray[.."); - w.Write(callName); - w.Write(".Length]\n : (__"); - w.Write(localName); - w.Write("_arrayFromPool = global::System.Buffers.ArrayPool<"); - w.Write(storageT); - w.Write(">.Shared.Rent("); - w.Write(callName); - w.Write(".Length));\n"); - - if (IsString(szArr.BaseType) && cat == ParamCategory.PassArray) - { - // Strings need an additional InlineArray16 + InlineArray16 (pinned handles). - // Only required for PassArray (managed -> HSTRING conversion); FillArray's native side - // fills HSTRING handles directly into the nint storage. - w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); - w.Write(localName); - w.Write("_inlineHeaderArray);\n"); - w.Write(" HStringHeader[] __"); - w.Write(localName); - w.Write("_headerArrayFromPool = null;\n"); - w.Write(" Span __"); - w.Write(localName); - w.Write("_headerSpan = "); - w.Write(callName); - w.Write(".Length <= 16\n ? __"); - w.Write(localName); - w.Write("_inlineHeaderArray[.."); - w.Write(callName); - w.Write(".Length]\n : (__"); - w.Write(localName); - w.Write("_headerArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); - w.Write(callName); - w.Write(".Length));\n"); - - w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); - w.Write(localName); - w.Write("_inlinePinnedHandleArray);\n"); - w.Write(" nint[] __"); - w.Write(localName); - w.Write("_pinnedHandleArrayFromPool = null;\n"); - w.Write(" Span __"); - w.Write(localName); - w.Write("_pinnedHandleSpan = "); - w.Write(callName); - w.Write(".Length <= 16\n ? __"); - w.Write(localName); - w.Write("_inlinePinnedHandleArray[.."); - w.Write(callName); - w.Write(".Length]\n : (__"); - w.Write(localName); - w.Write("_pinnedHandleArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); - w.Write(callName); - w.Write(".Length));\n"); - } - } - if (returnIsReceiveArray) - { - AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt!; - w.Write(" uint __retval_length = default;\n"); - w.Write(" "); - if (IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType)) - { - w.Write("void*"); - } - else if (IsComplexStruct(retSz.BaseType)) - { - w.Write(GetAbiStructTypeName(w, retSz.BaseType)); - } - else if (IsHResultException(retSz.BaseType)) - { - w.Write("global::ABI.System.Exception"); - } - else if (IsMappedAbiValueType(retSz.BaseType)) - { - w.Write(GetMappedAbiTypeName(retSz.BaseType)); - } - else if (IsAnyStruct(retSz.BaseType)) - { - w.Write(GetBlittableStructAbiType(w, retSz.BaseType)); - } - else - { - w.Write(GetAbiPrimitiveType(retSz.BaseType)); - } - w.Write("* __retval_data = default;\n"); - } - else if (returnIsHResultException) - { - w.Write(" global::ABI.System.Exception __retval = default;\n"); - } - else if (returnIsString || returnIsRefType) - { - w.Write(" void* __retval = default;\n"); - } - else if (returnIsAnyStruct) - { - w.Write(" "); - w.Write(GetBlittableStructAbiType(w, rt!)); - w.Write(" __retval = default;\n"); - } - else if (returnIsComplexStruct) - { - w.Write(" "); - w.Write(GetAbiStructTypeName(w, rt!)); - w.Write(" __retval = default;\n"); - } - else if (rt is not null && IsMappedAbiValueType(rt)) - { - // Mapped value type return (e.g. DateTime/TimeSpan): use the ABI struct as __retval. - w.Write(" "); - w.Write(GetMappedAbiTypeName(rt)); - w.Write(" __retval = default;\n"); - } - else if (rt is not null && IsSystemType(rt)) - { - // System.Type return: use ABI Type struct as __retval. - w.Write(" global::ABI.System.Type __retval = default;\n"); - } - else if (rt is not null) - { - w.Write(" "); - w.Write(GetAbiPrimitiveType(rt)); - w.Write(" __retval = default;\n"); - } - - // Determine if we need a try/finally (for cleanup of string/refType return or receive array - // return or Out runtime class params). Input string params no longer need try/finally — - // they use the HString fast-path (stack-allocated HStringReference, no free needed). - bool hasOutNeedsCleanup = false; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); - if (IsString(uOut) || IsRuntimeClassOrInterface(uOut) || IsObject(uOut) || IsSystemType(uOut) || IsComplexStruct(uOut) || IsGenericInstance(uOut)) { hasOutNeedsCleanup = true; break; } - } - bool hasReceiveArray = false; - for (int i = 0; i < sig.Params.Count; i++) - { - if (ParamHelpers.GetParamCategory(sig.Params[i]) == ParamCategory.ReceiveArray) { hasReceiveArray = true; break; } - } - bool hasNonBlittablePassArray = false; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if ((cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - && p.Type is AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArrCheck - && !IsBlittablePrimitive(szArrCheck.BaseType) && !IsAnyStruct(szArrCheck.BaseType) - && !IsMappedAbiValueType(szArrCheck.BaseType)) - { - hasNonBlittablePassArray = true; break; - } - } - bool hasComplexStructInput = false; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if ((cat == ParamCategory.In || cat == ParamCategory.Ref) && IsComplexStruct(StripByRefAndCustomModifiers(p.Type))) { hasComplexStructInput = true; break; } - } - // System.Type return: ABI.System.Type contains an HSTRING that must be disposed - // after marshalling to managed System.Type, otherwise the HSTRING leaks. Mirrors - // C++ abi_marshaler::write_dispose path for is_out + non-empty marshaler_type. - bool returnIsSystemTypeForCleanup = rt is not null && IsSystemType(rt); - bool needsTryFinally = returnIsString || returnIsRefType || returnIsReceiveArray || hasOutNeedsCleanup || hasReceiveArray || returnIsComplexStruct || hasNonBlittablePassArray || hasComplexStructInput || returnIsSystemTypeForCleanup; - if (needsTryFinally) { w.Write(" try\n {\n"); } - - string indent = needsTryFinally ? " " : " "; - - // Inside try (if applicable): assign complex-struct input locals via marshaller. - // Mirrors truth pattern: '__value = ProfileUsageMarshaller.ConvertToUnmanaged(value);' - // Includes both 'in' (ParamCategory.In) and 'in T' (ParamCategory.Ref) forms. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.In && cat != ParamCategory.Ref) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature pType = StripByRefAndCustomModifiers(p.Type); - if (!IsComplexStruct(pType)) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - w.Write(indent); - w.Write("__"); - w.Write(localName); - w.Write(" = "); - w.Write(GetMarshallerFullName(w, pType)); - w.Write(".ConvertToUnmanaged("); - w.Write(callName); - w.Write(");\n"); - } - // Type input params: set up TypeReference locals before the fixed block. Mirrors truth: - // global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe(forType, out TypeReference __forType); - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - if (ParamHelpers.GetParamCategory(p) != ParamCategory.In) { continue; } - if (!IsSystemType(p.Type)) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - string callName = GetParamName(p, paramNameOverride); - w.Write(indent); - w.Write("global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe("); - w.Write(callName); - w.Write(", out TypeReference __"); - w.Write(localName); - w.Write(");\n"); - } - // Open a SINGLE fixed-block for ALL pinnable inputs (mirrors C++ write_abi_invoke): - // 1. Ref params (typed ptr, separate "fixed(T* _x = &x)\n" lines, no braces) - // 2. Complex-struct PassArrays (typed ptr, separate fixed line) - // 3. All other "void*"-style pinnables (strings, Type[], blittable PassArrays, - // reference-type PassArrays via inline-pool span) merged into ONE - // "fixed(void* _a = ..., _b = ..., ...) {\n" block. - // - // C# allows multiple chained "fixed(...)" without braces to share the next braced - // body, which is what the C++ tool emits. This avoids the deep nesting mine had - // when emitting a separate fixed block per PassArray. - int fixedNesting = 0; - - // Step 1: Emit typed-pointer fixed lines for Ref params and complex-struct PassArrays - // (no braces - they share the body of the upcoming combined fixed-void* block, OR - // each other if no void* block is needed). - bool hasAnyVoidStarPinnable = false; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (IsString(p.Type) || IsSystemType(p.Type)) { hasAnyVoidStarPinnable = true; continue; } - if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - { - // All PassArrays (including complex structs) go in the void* combined block, - // matching truth's pattern. Complex structs use a (T*) cast at the call site. - hasAnyVoidStarPinnable = true; - } - } - // Emit typed fixed lines for Ref params. - // Skip Ref+ComplexStruct: those are marshalled via __local (no fixed needed) and - // passed as &__local at the call site, mirroring C++ tool's is_value_type_in path. - int typedFixedCount = 0; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat == ParamCategory.Ref) - { - AsmResolver.DotNet.Signatures.TypeSignature uRefSkip = StripByRefAndCustomModifiers(p.Type); - if (IsComplexStruct(uRefSkip)) { continue; } - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.TypeSignature uRef = uRefSkip; - string abiType = IsAnyStruct(uRef) ? GetBlittableStructAbiType(w, uRef) : GetAbiPrimitiveType(uRef); - w.Write(indent); - w.Write(new string(' ', fixedNesting * 4)); - w.Write("fixed("); - w.Write(abiType); - w.Write("* _"); - w.Write(localName); - w.Write(" = &"); - w.Write(callName); - w.Write(")\n"); - typedFixedCount++; - } - } - - // Step 2: Emit ONE combined fixed-void* block for all pinnables that share the - // same scope. Each variable is "_localName = rhsExpr". Strings get an extra - // "_localName_inlineHeaderArray = __localName_headerSpan" entry. - bool stringPinnablesEmitted = false; - if (hasAnyVoidStarPinnable) - { - w.Write(indent); - w.Write(new string(' ', fixedNesting * 4)); - w.Write("fixed(void* "); - bool first = true; - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - bool isString = IsString(p.Type); - bool isType = IsSystemType(p.Type); - bool isPassArray = cat == ParamCategory.PassArray || cat == ParamCategory.FillArray; - if (!isString && !isType && !isPassArray) { continue; } - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - if (!first) { w.Write(", "); } - first = false; - w.Write("_"); - w.Write(localName); - w.Write(" = "); - if (isType) - { - w.Write("__"); - w.Write(localName); - } - else if (isPassArray) - { - AsmResolver.DotNet.Signatures.TypeSignature elemT = ((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType; - bool isBlittableElem = IsBlittablePrimitive(elemT) || IsAnyStruct(elemT); - bool isStringElem = IsString(elemT); - if (isBlittableElem) - { - w.Write(callName); - } - else - { - w.Write("__"); - w.Write(localName); - w.Write("_span"); - } - // For string elements: only PassArray needs the additional inlineHeaderArray - // pinned alongside the data span. FillArray fills HSTRINGs into the nint - // storage directly (no header conversion needed). - if (isStringElem && cat == ParamCategory.PassArray) - { - w.Write(", _"); - w.Write(localName); - w.Write("_inlineHeaderArray = __"); - w.Write(localName); - w.Write("_headerSpan"); - } - } - else - { - // string param - w.Write(callName); - } - } - w.Write(")\n"); - w.Write(indent); - w.Write(new string(' ', fixedNesting * 4)); - w.Write("{\n"); - fixedNesting++; - // Inside the body: emit HStringMarshaller calls for input string params. - for (int i = 0; i < sig.Params.Count; i++) - { - if (!IsString(sig.Params[i].Type)) { continue; } - string callName = GetParamName(sig.Params[i], paramNameOverride); - string localName = GetParamLocalName(sig.Params[i], paramNameOverride); - w.Write(indent); - w.Write(new string(' ', fixedNesting * 4)); - w.Write("HStringMarshaller.ConvertToUnmanagedUnsafe((char*)_"); - w.Write(localName); - w.Write(", "); - w.Write(callName); - w.Write("?.Length, out HStringReference __"); - w.Write(localName); - w.Write(");\n"); - } - stringPinnablesEmitted = true; - } - else if (typedFixedCount > 0) - { - // Typed fixed lines exist but no void* combined block - we need a body block - // to host them. Open a brace block after the last typed fixed line. - w.Write(indent); - w.Write(new string(' ', fixedNesting * 4)); - w.Write("{\n"); - fixedNesting++; - } - // Suppress unused variable warning when block above doesn't fire. - _ = stringPinnablesEmitted; - - string callIndent = indent + new string(' ', fixedNesting * 4); - - // For non-blittable PassArray params, emit CopyToUnmanaged_ (UnsafeAccessor) and call - // it to populate the inline/pooled storage from the user-supplied span. For string arrays, - // use HStringArrayMarshaller.ConvertToUnmanagedUnsafe instead. - // FillArray of strings is the exception: the native side fills the HSTRING handles, so - // there's nothing to convert pre-call (the post-call CopyToManaged_ handles writeback). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - if (IsString(szArr.BaseType)) - { - // Skip pre-call ConvertToUnmanagedUnsafe for FillArray of strings — there's - // nothing to convert (native fills the handles). Mirrors C++ truth pattern. - if (cat == ParamCategory.FillArray) { continue; } - w.Write(callIndent); - w.Write("HStringArrayMarshaller.ConvertToUnmanagedUnsafe(\n"); - w.Write(callIndent); - w.Write(" source: "); - w.Write(callName); - w.Write(",\n"); - w.Write(callIndent); - w.Write(" hstringHeaders: (HStringHeader*) _"); - w.Write(localName); - w.Write("_inlineHeaderArray,\n"); - w.Write(callIndent); - w.Write(" hstrings: __"); - w.Write(localName); - w.Write("_span,\n"); - w.Write(callIndent); - w.Write(" pinnedGCHandles: __"); - w.Write(localName); - w.Write("_pinnedHandleSpan);\n"); - } - else - { - // FillArray (Span) of non-blittable element types: skip pre-call - // CopyToUnmanaged. The buffer the native side gets (_) is uninitialized - // ABI-format storage; the native callee fills it. The post-call writeback loop - // emits CopyToManaged_ to propagate the native fills into the user's - // managed Span. (Mirrors C++ marshaler.write_marshal_to_abi which only emits - // CopyToUnmanaged for PassArray, not FillArray.) - if (cat == ParamCategory.FillArray) { continue; } - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); - string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); - // For mapped value types (DateTime/TimeSpan) and complex structs, the storage - // element is the ABI struct type; the data pointer parameter type uses that - // ABI struct. The fixed() opens with void* (per truth's pattern), so a cast - // is required at the call site. For runtime classes/objects, use void**. - string dataParamType; - string dataCastType; - if (IsMappedAbiValueType(szArr.BaseType)) - { - dataParamType = GetMappedAbiTypeName(szArr.BaseType) + "*"; - dataCastType = "(" + GetMappedAbiTypeName(szArr.BaseType) + "*)"; - } - else if (IsHResultException(szArr.BaseType)) - { - dataParamType = "global::ABI.System.Exception*"; - dataCastType = "(global::ABI.System.Exception*)"; - } - else if (IsComplexStruct(szArr.BaseType)) - { - string abiStructName = GetAbiStructTypeName(w, szArr.BaseType); - dataParamType = abiStructName + "*"; - dataCastType = "(" + abiStructName + "*)"; - } - else - { - dataParamType = "void**"; - dataCastType = "(void**)"; - } - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToUnmanaged\")]\n"); - w.Write(callIndent); - w.Write("static extern void CopyToUnmanaged_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); - w.Write("\")] object _, ReadOnlySpan<"); - w.Write(elementProjected); - w.Write("> span, uint length, "); - w.Write(dataParamType); - w.Write(" data);\n"); - w.Write(callIndent); - w.Write("CopyToUnmanaged_"); - w.Write(localName); - w.Write("(null, "); - w.Write(callName); - w.Write(", (uint)"); - w.Write(callName); - w.Write(".Length, "); - w.Write(dataCastType); - w.Write("_"); - w.Write(localName); - w.Write(");\n"); - } - } - - w.Write(callIndent); - // Mirrors C++ code_writers.h:6725 - omit the ThrowExceptionForHR wrap when the - // method/property is [NoException] (its HRESULT is contractually S_OK). - if (!isNoExcept) - { - w.Write("RestrictedErrorInfo.ThrowExceptionForHR((*(delegate* unmanaged[MemberFunction]<"); - } - else - { - w.Write("(*(delegate* unmanaged[MemberFunction]<"); - } - w.Write(fp.ToString()); - w.Write(">**)ThisPtr)["); - w.Write(slot); - w.Write("](ThisPtr"); - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - { - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - w.Write(",\n (uint)"); - w.Write(callName); - w.Write(".Length, _"); - w.Write(localName); - continue; - } - if (cat == ParamCategory.Out) - { - string localName = GetParamLocalName(p, paramNameOverride); - w.Write(",\n &__"); - w.Write(localName); - continue; - } - if (cat == ParamCategory.ReceiveArray) - { - string localName = GetParamLocalName(p, paramNameOverride); - w.Write(",\n &__"); - w.Write(localName); - w.Write("_length, &__"); - w.Write(localName); - w.Write("_data"); - continue; - } - if (cat == ParamCategory.Ref) - { - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.TypeSignature uRefArg = StripByRefAndCustomModifiers(p.Type); - if (IsComplexStruct(uRefArg)) - { - // Complex struct 'in' (Ref) param: pass &__local (the marshaled ABI struct). - w.Write(",\n &__"); - w.Write(localName); - } - else - { - // 'in T' projected param: pass the pinned pointer. - w.Write(",\n _"); - w.Write(localName); - } - continue; - } - w.Write(",\n "); - if (IsHResultException(p.Type)) - { - w.Write("__"); - w.Write(GetParamLocalName(p, paramNameOverride)); - } - else if (IsString(p.Type)) - { - w.Write("__"); - w.Write(GetParamLocalName(p, paramNameOverride)); - w.Write(".HString"); - } - else if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type) || IsGenericInstance(p.Type)) - { - w.Write("__"); - w.Write(GetParamLocalName(p, paramNameOverride)); - w.Write(".GetThisPtrUnsafe()"); - } - else if (IsSystemType(p.Type)) - { - // System.Type input: pass the pre-converted ABI Type struct (via the local set up before the call). - w.Write("__"); - w.Write(GetParamLocalName(p, paramNameOverride)); - w.Write(".ConvertToUnmanagedUnsafe()"); - } - else if (IsMappedAbiValueType(p.Type)) - { - // Mapped value-type input: pass the pre-converted ABI local. - w.Write("__"); - w.Write(GetParamLocalName(p, paramNameOverride)); - } - else if (IsComplexStruct(p.Type)) - { - // Complex struct input: pass the pre-converted ABI struct local. - w.Write("__"); - w.Write(GetParamLocalName(p, paramNameOverride)); - } - else if (IsAnyStruct(p.Type)) - { - w.Write(GetParamName(p, paramNameOverride)); - } - else - { - EmitParamArgConversion(w, p, paramNameOverride); - } - } - if (returnIsReceiveArray) - { - w.Write(",\n &__retval_length, &__retval_data"); - } - else if (rt is not null) - { - w.Write(",\n &__retval"); - } - // Close the vtable call. One less ')' when noexcept (no ThrowExceptionForHR wrap). - w.Write(isNoExcept ? ");\n" : "));\n"); - - // After call: copy native-filled values back into the user's managed Span for - // FillArray of non-blittable element types. The native callee wrote into our - // ABI-format buffer (_) which is separate from the user's Span; we need to - // CopyToManaged_ to convert each ABI element back to the projected form and - // store it in the user's Span. Mirrors C++ marshaler.write_marshal_from_abi - // (code_writers.h:6268). - // Blittable element types (primitives and almost-blittable structs) don't need this - // because the user's Span wraps the same memory the native side wrote to. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szFA) { continue; } - if (IsBlittablePrimitive(szFA.BaseType) || IsAnyStruct(szFA.BaseType)) { continue; } - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szFA.BaseType)))); - string elementInteropArg = EncodeInteropTypeName(szFA.BaseType, TypedefNameType.Projected); - // Determine the ABI element type for the data pointer parameter. - // - Strings / runtime classes / objects: void** - // - HResult exception: global::ABI.System.Exception* - // - Mapped value types: global::ABI.System.{DateTimeOffset|TimeSpan}* - // - Complex structs: * - string dataParamType; - string dataCastType; - if (IsString(szFA.BaseType) || IsRuntimeClassOrInterface(szFA.BaseType) || IsObject(szFA.BaseType)) - { - dataParamType = "void** data"; - dataCastType = "(void**)"; - } - else if (IsHResultException(szFA.BaseType)) - { - dataParamType = "global::ABI.System.Exception* data"; - dataCastType = "(global::ABI.System.Exception*)"; - } - else if (IsMappedAbiValueType(szFA.BaseType)) - { - string abiName = GetMappedAbiTypeName(szFA.BaseType); - dataParamType = abiName + "* data"; - dataCastType = "(" + abiName + "*)"; - } - else - { - string abiStructName = GetAbiStructTypeName(w, szFA.BaseType); - dataParamType = abiStructName + "* data"; - dataCastType = "(" + abiStructName + "*)"; - } - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToManaged\")]\n"); - w.Write(callIndent); - w.Write("static extern void CopyToManaged_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szFA.BaseType, elementInteropArg)); - w.Write("\")] object _, uint length, "); - w.Write(dataParamType); - w.Write(", Span<"); - w.Write(elementProjected); - w.Write("> span);\n"); - w.Write(callIndent); - w.Write("CopyToManaged_"); - w.Write(localName); - w.Write("(null, (uint)__"); - w.Write(localName); - w.Write("_span.Length, "); - w.Write(dataCastType); - w.Write("_"); - w.Write(localName); - w.Write(", "); - w.Write(callName); - w.Write(");\n"); - } - - // After call: write back Out params to caller's 'out' var. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); - - // For Out generic instance: emit inline UnsafeAccessor to ConvertToManaged_ - // before the writeback. Mirrors the truth pattern (e.g. Collection1HandlerInvoke - // emits the accessor inside try, right before the assignment). - if (IsGenericInstance(uOut)) - { - string interopTypeName = EncodeInteropTypeName(uOut, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, uOut, false))); - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); - w.Write(callIndent); - w.Write("static extern "); - w.Write(projectedTypeName); - w.Write(" ConvertToManaged_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, void* value);\n"); - w.Write(callIndent); - w.Write(callName); - w.Write(" = ConvertToManaged_"); - w.Write(localName); - w.Write("(null, __"); - w.Write(localName); - w.Write(");\n"); - continue; - } - - w.Write(callIndent); - w.Write(callName); - w.Write(" = "); - if (IsString(uOut)) - { - w.Write("HStringMarshaller.ConvertToManaged(__"); - w.Write(localName); - w.Write(")"); - } - else if (IsObject(uOut)) - { - w.Write("WindowsRuntimeObjectMarshaller.ConvertToManaged(__"); - w.Write(localName); - w.Write(")"); - } - else if (IsRuntimeClassOrInterface(uOut)) - { - w.Write(GetMarshallerFullName(w, uOut)); - w.Write(".ConvertToManaged(__"); - w.Write(localName); - w.Write(")"); - } - else if (IsSystemType(uOut)) - { - w.Write("global::ABI.System.TypeMarshaller.ConvertToManaged(__"); - w.Write(localName); - w.Write(")"); - } - else if (IsComplexStruct(uOut)) - { - w.Write(GetMarshallerFullName(w, uOut)); - w.Write(".ConvertToManaged(__"); - w.Write(localName); - w.Write(")"); - } - else if (IsAnyStruct(uOut)) - { - w.Write("__"); - w.Write(localName); - } - else if (uOut is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibBool && corlibBool.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) - { - w.Write("__"); - w.Write(localName); - } - else if (uOut is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibChar && corlibChar.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) - { - w.Write("__"); - w.Write(localName); - } - else if (IsEnumType(uOut)) - { - // Enum out param: __ local is already the projected enum type (since the - // function pointer signature uses the projected type). No cast needed. - w.Write("__"); - w.Write(localName); - } - else - { - w.Write("__"); - w.Write(localName); - } - w.Write(";\n"); - } - - // Writeback for ReceiveArray params: emit a UnsafeAccessor + assign to the out param. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.ReceiveArray) { continue; } - string callName = GetParamName(p, paramNameOverride); - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(sza.BaseType)))); - // Element ABI type: void* for ref types (string/runtime class/object); ABI struct - // type for complex structs (e.g. authored BasicStruct); blittable struct ABI for - // blittable structs; primitive ABI otherwise. - string elementAbi = IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType) - ? "void*" - : IsComplexStruct(sza.BaseType) - ? GetAbiStructTypeName(w, sza.BaseType) - : IsAnyStruct(sza.BaseType) - ? GetBlittableStructAbiType(w, sza.BaseType) - : GetAbiPrimitiveType(sza.BaseType); - string elementInteropArg = EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); - string marshallerPath = GetArrayMarshallerInteropPath(w, sza.BaseType, elementInteropArg); - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); - w.Write(callIndent); - w.Write("static extern "); - w.Write(elementProjected); - w.Write("[] ConvertToManaged_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(marshallerPath); - w.Write("\")] object _, uint length, "); - w.Write(elementAbi); - w.Write("* data);\n"); - w.Write(callIndent); - w.Write(callName); - w.Write(" = ConvertToManaged_"); - w.Write(localName); - w.Write("(null, __"); - w.Write(localName); - w.Write("_length, __"); - w.Write(localName); - w.Write("_data);\n"); - } - if (rt is not null) - { - if (returnIsReceiveArray) - { - AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt; - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(retSz.BaseType)))); - string elementAbi = IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType) - ? "void*" - : IsComplexStruct(retSz.BaseType) - ? GetAbiStructTypeName(w, retSz.BaseType) - : IsHResultException(retSz.BaseType) - ? "global::ABI.System.Exception" - : IsMappedAbiValueType(retSz.BaseType) - ? GetMappedAbiTypeName(retSz.BaseType) - : IsAnyStruct(retSz.BaseType) - ? GetBlittableStructAbiType(w, retSz.BaseType) - : GetAbiPrimitiveType(retSz.BaseType); - string elementInteropArg = EncodeInteropTypeName(retSz.BaseType, TypedefNameType.Projected); - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); - w.Write(callIndent); - w.Write("static extern "); - w.Write(elementProjected); - w.Write("[] ConvertToManaged_retval([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, retSz.BaseType, elementInteropArg)); - w.Write("\")] object _, uint length, "); - w.Write(elementAbi); - w.Write("* data);\n"); - w.Write(callIndent); - w.Write("return ConvertToManaged_retval(null, __retval_length, __retval_data);\n"); - } - else if (returnIsHResultException) - { - w.Write(callIndent); - w.Write("return global::ABI.System.ExceptionMarshaller.ConvertToManaged(__retval);\n"); - } - else if (returnIsString) - { - w.Write(callIndent); - w.Write("return HStringMarshaller.ConvertToManaged(__retval);\n"); - } - else if (returnIsRefType) - { - if (IsNullableT(rt)) - { - // Nullable return: use Marshaller.UnboxToManaged. Mirrors truth pattern; - // there is no NullableMarshaller, the inner-T marshaller has UnboxToManaged. - AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(rt)!; - string innerMarshaller = GetNullableInnerMarshallerName(w, inner); - w.Write(callIndent); - w.Write("return "); - w.Write(innerMarshaller); - w.Write(".UnboxToManaged(__retval);\n"); - } - else if (IsGenericInstance(rt)) - { - string interopTypeName = EncodeInteropTypeName(rt, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt, false))); - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToManaged\")]\n"); - w.Write(callIndent); - w.Write("static extern "); - w.Write(projectedTypeName); - w.Write(" ConvertToManaged_retval([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, void* value);\n"); - w.Write(callIndent); - w.Write("return ConvertToManaged_retval(null, __retval);\n"); - } - else - { - w.Write(callIndent); - w.Write("return "); - EmitMarshallerConvertToManaged(w, rt, "__retval"); - w.Write(";\n"); - } - } - else if (rt is not null && IsMappedAbiValueType(rt)) - { - // Mapped value type return (e.g. DateTime/TimeSpan): convert ABI struct back via marshaller. - w.Write(callIndent); - w.Write("return "); - w.Write(GetMappedMarshallerName(rt)); - w.Write(".ConvertToManaged(__retval);\n"); - } - else if (rt is not null && IsSystemType(rt)) - { - // System.Type return: convert ABI Type struct back to System.Type via TypeMarshaller. - w.Write(callIndent); - w.Write("return global::ABI.System.TypeMarshaller.ConvertToManaged(__retval);\n"); - } - else if (returnIsAnyStruct) - { - w.Write(callIndent); - if (rt is not null && IsMappedAbiValueType(rt)) - { - // Mapped value type return: convert ABI struct back to projected via marshaller. - w.Write("return "); - w.Write(GetMappedMarshallerName(rt)); - w.Write(".ConvertToManaged(__retval);\n"); - } - else - { - w.Write("return __retval;\n"); - } - } - else if (returnIsComplexStruct) - { - w.Write(callIndent); - w.Write("return "); - w.Write(GetMarshallerFullName(w, rt!)); - w.Write(".ConvertToManaged(__retval);\n"); - } - else - { - w.Write(callIndent); - w.Write("return "); - string projected = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, rt!, false))); - string abiType = GetAbiPrimitiveType(rt!); - if (projected == abiType) { w.Write("__retval;\n"); } - else - { - w.Write("("); - w.Write(projected); - w.Write(")__retval;\n"); - } - } - } - - // Close fixed blocks (innermost first). - for (int i = fixedNesting - 1; i >= 0; i--) - { - w.Write(indent); - w.Write(new string(' ', i * 4)); - w.Write("}\n"); - } - - if (needsTryFinally) - { - w.Write(" }\n finally\n {\n"); - - // Order matches truth (mirrors C++ disposer iteration order): - // 0. Complex-struct input param Dispose (e.g. ProfileUsageMarshaller.Dispose(__value)) - // 1. Non-blittable PassArray/FillArray cleanup (Dispose + ArrayPools) - // 2. Out param frees (HString / object / runtime class) - // 3. ReceiveArray param frees (Free_ via UnsafeAccessor) - // 4. Return free (__retval) — last - - // 0. Dispose complex-struct input params via marshaller (both 'in' and 'in T' forms). - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.In && cat != ParamCategory.Ref) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature pType = StripByRefAndCustomModifiers(p.Type); - if (!IsComplexStruct(pType)) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - w.Write(" "); - w.Write(GetMarshallerFullName(w, pType)); - w.Write(".Dispose(__"); - w.Write(localName); - w.Write(");\n"); - } - // 1. Cleanup non-blittable PassArray/FillArray params: - // For strings: HStringArrayMarshaller.Dispose + return ArrayPools (3 of them). - // For runtime classes/objects: Dispose_ (UnsafeAccessor) + return ArrayPool. - // For mapped value types (DateTime/TimeSpan): no per-element disposal needed and truth - // doesn't return the ArrayPool either, so skip entirely. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - if (IsMappedAbiValueType(szArr.BaseType)) { continue; } - if (IsHResultException(szArr.BaseType)) - { - // HResultException ABI is just an int; per-element Dispose is a no-op (mirror - // the truth: no Dispose_ emitted). Just return the inline-array's pool - // using the correct element type (ABI.System.Exception, not nint). - string localNameH = GetParamLocalName(p, paramNameOverride); - w.Write("\n if (__"); - w.Write(localNameH); - w.Write("_arrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(localNameH); - w.Write("_arrayFromPool);\n }\n"); - continue; - } - string localName = GetParamLocalName(p, paramNameOverride); - if (IsString(szArr.BaseType)) - { - // The HStringArrayMarshaller.Dispose + ArrayPool returns for strings only - // apply to PassArray (where we set up the pinned handles + headers in the - // first place). FillArray writes back HSTRING handles into the nint storage - // array directly, with no per-element pinned handle / header to release. - if (cat == ParamCategory.PassArray) - { - w.Write(" HStringArrayMarshaller.Dispose(__"); - w.Write(localName); - w.Write("_pinnedHandleSpan);\n\n"); - w.Write(" if (__"); - w.Write(localName); - w.Write("_pinnedHandleArrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(localName); - w.Write("_pinnedHandleArrayFromPool);\n }\n\n"); - w.Write(" if (__"); - w.Write(localName); - w.Write("_headerArrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(localName); - w.Write("_headerArrayFromPool);\n }\n"); - } - // Both PassArray and FillArray need the inline-array's nint pool returned. - w.Write("\n if (__"); - w.Write(localName); - w.Write("_arrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(localName); - w.Write("_arrayFromPool);\n }\n"); - } - else - { - // For complex structs, both the Dispose_ data param and the fixed() - // pointer must be typed as *; the cast can be omitted. For - // runtime classes / objects / strings the data is void** and the fixed() - // remains void* with a (void**) cast. - string disposeDataParamType; - string fixedPtrType; - string disposeCastType; - if (IsComplexStruct(szArr.BaseType)) - { - string abiStructName = GetAbiStructTypeName(w, szArr.BaseType); - disposeDataParamType = abiStructName + "*"; - fixedPtrType = abiStructName + "*"; - disposeCastType = string.Empty; - } - else - { - disposeDataParamType = "void** data"; - fixedPtrType = "void*"; - disposeCastType = "(void**)"; - } - string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Dispose\")]\n"); - w.Write(" static extern void Dispose_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); - w.Write("\")] object _, uint length, "); - w.Write(disposeDataParamType); - if (!disposeDataParamType.EndsWith("data", System.StringComparison.Ordinal)) { w.Write(" data"); } - w.Write(");\n\n"); - w.Write(" fixed("); - w.Write(fixedPtrType); - w.Write(" _"); - w.Write(localName); - w.Write(" = __"); - w.Write(localName); - w.Write("_span)\n {\n"); - w.Write(" Dispose_"); - w.Write(localName); - w.Write("(null, (uint) __"); - w.Write(localName); - w.Write("_span.Length, "); - w.Write(disposeCastType); - w.Write("_"); - w.Write(localName); - w.Write(");\n }\n"); - } - // ArrayPool storage type matches the InlineArray storage (mapped ABI value type - // for DateTime/TimeSpan; ABI struct for complex structs; nint otherwise). - string poolStorageT = IsMappedAbiValueType(szArr.BaseType) - ? GetMappedAbiTypeName(szArr.BaseType) - : IsComplexStruct(szArr.BaseType) - ? GetAbiStructTypeName(w, szArr.BaseType) - : "nint"; - w.Write("\n if (__"); - w.Write(localName); - w.Write("_arrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool<"); - w.Write(poolStorageT); - w.Write(">.Shared.Return(__"); - w.Write(localName); - w.Write("_arrayFromPool);\n }\n"); - } - - // 2. Free Out string/object/runtime-class params. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.Out) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature uOut = StripByRefAndCustomModifiers(p.Type); - string localName = GetParamLocalName(p, paramNameOverride); - if (IsString(uOut)) - { - w.Write(" HStringMarshaller.Free(__"); - w.Write(localName); - w.Write(");\n"); - } - else if (IsObject(uOut) || IsRuntimeClassOrInterface(uOut) || IsGenericInstance(uOut)) - { - w.Write(" WindowsRuntimeUnknownMarshaller.Free(__"); - w.Write(localName); - w.Write(");\n"); - } - else if (IsSystemType(uOut)) - { - w.Write(" global::ABI.System.TypeMarshaller.Dispose(__"); - w.Write(localName); - w.Write(");\n"); - } - else if (IsComplexStruct(uOut)) - { - w.Write(" "); - w.Write(GetMarshallerFullName(w, uOut)); - w.Write(".Dispose(__"); - w.Write(localName); - w.Write(");\n"); - } - } - - // 3. Free ReceiveArray params via UnsafeAccessor. - for (int i = 0; i < sig.Params.Count; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.ReceiveArray) { continue; } - string localName = GetParamLocalName(p, paramNameOverride); - AsmResolver.DotNet.Signatures.SzArrayTypeSignature sza = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)StripByRefAndCustomModifiers(p.Type); - // Element ABI type: void* for ref types; ABI struct for complex/blittable structs; - // primitive ABI otherwise. (Same categorization as the ConvertToManaged_ path.) - string elementAbi = IsString(sza.BaseType) || IsRuntimeClassOrInterface(sza.BaseType) || IsObject(sza.BaseType) - ? "void*" - : IsComplexStruct(sza.BaseType) - ? GetAbiStructTypeName(w, sza.BaseType) - : IsAnyStruct(sza.BaseType) - ? GetBlittableStructAbiType(w, sza.BaseType) - : GetAbiPrimitiveType(sza.BaseType); - string elementInteropArg = EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); - string marshallerPath = GetArrayMarshallerInteropPath(w, sza.BaseType, elementInteropArg); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Free\")]\n"); - w.Write(" static extern void Free_"); - w.Write(localName); - w.Write("([UnsafeAccessorType(\""); - w.Write(marshallerPath); - w.Write("\")] object _, uint length, "); - w.Write(elementAbi); - w.Write("* data);\n\n"); - w.Write(" Free_"); - w.Write(localName); - w.Write("(null, __"); - w.Write(localName); - w.Write("_length, __"); - w.Write(localName); - w.Write("_data);\n"); - } - - // 4. Free return value (__retval) — emitted last to match truth ordering. - if (returnIsString) - { - w.Write(" HStringMarshaller.Free(__retval);\n"); - } - else if (returnIsRefType) - { - w.Write(" WindowsRuntimeUnknownMarshaller.Free(__retval);\n"); - } - else if (returnIsComplexStruct) - { - w.Write(" "); - w.Write(GetMarshallerFullName(w, rt!)); - w.Write(".Dispose(__retval);\n"); - } - else if (returnIsSystemTypeForCleanup) - { - // System.Type return: dispose the ABI.System.Type's HSTRING fields. - w.Write(" global::ABI.System.TypeMarshaller.Dispose(__retval);\n"); - } - else if (returnIsReceiveArray) - { - AsmResolver.DotNet.Signatures.SzArrayTypeSignature retSz = (AsmResolver.DotNet.Signatures.SzArrayTypeSignature)rt!; - string elementAbi = IsString(retSz.BaseType) || IsRuntimeClassOrInterface(retSz.BaseType) || IsObject(retSz.BaseType) - ? "void*" - : IsComplexStruct(retSz.BaseType) - ? GetAbiStructTypeName(w, retSz.BaseType) - : IsHResultException(retSz.BaseType) - ? "global::ABI.System.Exception" - : IsMappedAbiValueType(retSz.BaseType) - ? GetMappedAbiTypeName(retSz.BaseType) - : IsAnyStruct(retSz.BaseType) - ? GetBlittableStructAbiType(w, retSz.BaseType) - : GetAbiPrimitiveType(retSz.BaseType); - string elementInteropArg = EncodeInteropTypeName(retSz.BaseType, TypedefNameType.Projected); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Free\")]\n"); - w.Write(" static extern void Free_retval([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, retSz.BaseType, elementInteropArg)); - w.Write("\")] object _, uint length, "); - w.Write(elementAbi); - w.Write("* data);\n"); - w.Write(" Free_retval(null, __retval_length, __retval_data);\n"); - } - - w.Write(" }\n"); - } - - w.Write(" }\n"); - } - - /// True if the type signature is a Nullable<T> where T is a primitive - /// supported by an ABI.System.<T>Marshaller (e.g. UInt64Marshaller, Int32Marshaller, etc.). - /// Returns the fully-qualified marshaller name in . - private static bool TryGetNullablePrimitiveMarshallerName(AsmResolver.DotNet.Signatures.TypeSignature sig, out string? marshallerName) - { - marshallerName = null; - if (sig is not AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) { return false; } - var gt = gi.GenericType; - string ns = gt?.Namespace?.Value ?? string.Empty; - string name = gt?.Name?.Value ?? string.Empty; - // In WinMD metadata, Nullable is encoded as Windows.Foundation.IReference. - // It only later gets projected to System.Nullable by the projection layer. - bool isNullable = (ns == "System" && name == "Nullable`1") - || (ns == "Windows.Foundation" && name == "IReference`1"); - if (!isNullable) { return false; } - if (gi.TypeArguments.Count != 1) { return false; } - AsmResolver.DotNet.Signatures.TypeSignature arg = gi.TypeArguments[0]; - // Map primitive corlib element type to its ABI marshaller name. - if (arg is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) - { - string? mn = corlib.ElementType switch - { - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean => "Boolean", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char => "Char", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => "SByte", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => "Byte", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => "Int16", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => "UInt16", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => "Int32", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => "UInt32", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => "Int64", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => "UInt64", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 => "Single", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 => "Double", - _ => null - }; - if (mn is null) { return false; } - marshallerName = "ABI.System." + mn + "Marshaller"; - return true; - } - return false; - } - - /// True if the type signature represents the System.Object root type. - private static bool IsObject(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - return sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && - corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object; - } - - /// True if the type signature represents Windows.Foundation.HResult / System.Exception - /// (special-cased: ABI is global::ABI.System.Exception (an HResult struct), projected is Exception, - /// requires custom marshalling via ABI.System.ExceptionMarshaller). - private static bool IsHResultException(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } - string ns = td.Type?.Namespace?.Value ?? string.Empty; - string name = td.Type?.Name?.Value ?? string.Empty; - return (ns == "System" && name == "Exception") - || (ns == "Windows.Foundation" && name == "HResult"); - } - - /// - /// True if the type is a mapped value type that requires marshalling between projected and ABI - /// representations (e.g. Windows.Foundation.DateTime <-> System.DateTimeOffset, - /// Windows.Foundation.TimeSpan <-> System.TimeSpan, Windows.Foundation.HResult <-> System.Exception). - /// These types use 'global::ABI.<MappedNamespace>.<MappedName>' as their ABI representation - /// and need an explicit marshaller call ('global::ABI.<MappedNamespace>.<MappedName>Marshaller.ConvertToUnmanaged'/ - /// 'ConvertToManaged') to convert values across the boundary. - /// - private static bool IsMappedMarshalingValueType(AsmResolver.DotNet.Signatures.TypeSignature sig, out string mappedNs, out string mappedName) - { - mappedNs = string.Empty; - mappedName = string.Empty; - AsmResolver.DotNet.ITypeDefOrRef? td = null; - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature tds) { td = tds.Type; } - if (td is null) { return false; } - string ns = td.Namespace?.Value ?? string.Empty; - string name = td.Name?.Value ?? string.Empty; - // The set of mapped types that use the 'value-type marshaller' pattern (DateTime, TimeSpan, HResult). - // Uri is also a mapped marshalling type but it's a reference type (handled via UriMarshaller separately). - if (ns == "Windows.Foundation") - { - if (name == "DateTime") { mappedNs = "System"; mappedName = "DateTimeOffset"; return true; } - if (name == "TimeSpan") { mappedNs = "System"; mappedName = "TimeSpan"; return true; } - if (name == "HResult") { mappedNs = "System"; mappedName = "Exception"; return true; } - } - return false; - } - - /// True if the type is a mapped value type that needs ABI marshalling (excluding HResult, handled separately). - private static bool IsMappedAbiValueType(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (!IsMappedMarshalingValueType(sig, out _, out string mappedName)) { return false; } - // HResult/Exception is treated specially in many places; this helper is for DateTime/TimeSpan only. - return mappedName != "Exception"; - } - - /// Returns the ABI type name for a mapped value type (e.g. 'global::ABI.System.TimeSpan'). - private static string GetMappedAbiTypeName(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (!IsMappedMarshalingValueType(sig, out string ns, out string name)) { return string.Empty; } - return "global::ABI." + ns + "." + name; - } - - /// Returns the marshaller class name for a mapped value type (e.g. 'global::ABI.System.TimeSpanMarshaller'). - private static string GetMappedMarshallerName(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (!IsMappedMarshalingValueType(sig, out string ns, out string name)) { return string.Empty; } - return "global::ABI." + ns + "." + name + "Marshaller"; - } - - /// True if the type signature represents an enum (resolves cross-module typerefs). - private static bool IsEnumType(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } - if (td.Type is TypeDefinition def) - { - return TypeCategorization.GetCategory(def) == TypeCategory.Enum; - } - if (td.Type is TypeReference tr && _cacheRef is not null) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); - return resolved is not null && TypeCategorization.GetCategory(resolved) == TypeCategory.Enum; - } - return false; - } - - /// True if the type signature represents a generic instantiation that needs WinRT.Interop UnsafeAccessor marshalling. - private static bool IsGenericInstance(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - return sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; - } - - /// True if the signature is a WinRT IReference<T> (which projects to Nullable<T>). - private static bool IsNullableT(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is not AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) { return false; } - string ns = gi.GenericType?.Namespace?.Value ?? string.Empty; - string name = gi.GenericType?.Name?.Value ?? string.Empty; - return (ns == "Windows.Foundation" && name == "IReference`1") - || (ns == "System" && name == "Nullable`1"); - } - - /// Returns the inner type argument of a Nullable<T> signature (or the IReference variant). - private static AsmResolver.DotNet.Signatures.TypeSignature? GetNullableInnerType(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi && gi.TypeArguments.Count == 1) - { - return gi.TypeArguments[0]; - } - return null; - } - - /// Returns the marshaller name for the inner type T of Nullable<T>. - /// Mirrors the truth pattern: e.g. for Nullable<DateTimeOffset> returns - /// global::ABI.System.DateTimeOffsetMarshaller; for primitives like Nullable<int> - /// returns global::ABI.System.Int32Marshaller. - private static string GetNullableInnerMarshallerName(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature innerType) - { - // Primitives (Int32, Int64, Boolean, etc.) live in ABI.System with the canonical .NET name. - if (innerType is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) - { - string typeName = corlib.ElementType switch - { - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean => "Boolean", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char => "Char", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => "SByte", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => "Byte", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => "Int16", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => "UInt16", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => "Int32", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => "UInt32", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => "Int64", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => "UInt64", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 => "Single", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 => "Double", - _ => "", - }; - if (!string.IsNullOrEmpty(typeName)) - { - return "global::ABI.System." + typeName + "Marshaller"; - } - } - // For non-primitive types (DateTimeOffset, TimeSpan, struct/enum types), use GetMarshallerFullName. - return GetMarshallerFullName(w, innerType); - } - - /// Strips ByReferenceTypeSignature and CustomModifierTypeSignature wrappers - /// to get the underlying type signature. - private static AsmResolver.DotNet.Signatures.TypeSignature StripByRefAndCustomModifiers(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - AsmResolver.DotNet.Signatures.TypeSignature current = sig; - while (true) - { - if (current is AsmResolver.DotNet.Signatures.ByReferenceTypeSignature br) { current = br.BaseType; continue; } - if (current is AsmResolver.DotNet.Signatures.CustomModifierTypeSignature cm) { current = cm.BaseType; continue; } - return current; - } - } - - /// True if the type signature represents a WinRT runtime class, interface, or delegate (reference type marshallable via *Marshaller). - private static bool IsRuntimeClassOrInterface(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - // Same-module: use the resolved category directly. - if (td.Type is TypeDefinition def) - { - TypeCategory cat = TypeCategorization.GetCategory(def); - return cat is TypeCategory.Class or TypeCategory.Interface or TypeCategory.Delegate; - } - // Cross-module typeref: try to resolve via the metadata cache to check category. - string ns = td.Type?.Namespace?.Value ?? string.Empty; - string name = td.Type?.Name?.Value ?? string.Empty; - if (ns == "System") - { - return name switch - { - "Uri" or "Type" or "IDisposable" or "Exception" => true, - _ => false, - }; - } - if (_cacheRef is not null) - { - TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); - if (resolved is not null) - { - TypeCategory cat = TypeCategorization.GetCategory(resolved); - return cat is TypeCategory.Class or TypeCategory.Interface or TypeCategory.Delegate; - } - } - // Unresolved cross-assembly TypeRef (e.g. a referenced winmd we don't have loaded). - // Fall back to the signature's encoding: WinRT metadata distinguishes value types - // (encoded as ValueType) from reference types (encoded as Class). If the signature - // has IsValueType == false, then it MUST be one of class/interface/delegate (since - // primitives/enums/strings/object are encoded with their own element type). This - // mirrors how the C++ tool's abi_marshaler abstraction handles unknown types — it - // dispatches based on the metadata semantics, not on resolution. - return !td.IsValueType; - } - return false; - } - - /// Emits the call to the appropriate marshaller's ConvertToUnmanaged for a runtime class / object input parameter. - private static void EmitMarshallerConvertToUnmanaged(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig, string argName) - { - if (IsObject(sig)) - { - w.Write("WindowsRuntimeObjectMarshaller.ConvertToUnmanaged("); - w.Write(argName); - w.Write(")"); - return; - } - // Runtime class / interface: use ABI..Marshaller - w.Write(GetMarshallerFullName(w, sig)); - w.Write(".ConvertToUnmanaged("); - w.Write(argName); - w.Write(")"); - } - - /// Emits the call to the appropriate marshaller's ConvertToManaged for a runtime class / object return value. - private static void EmitMarshallerConvertToManaged(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig, string argName) - { - if (IsObject(sig)) - { - w.Write("WindowsRuntimeObjectMarshaller.ConvertToManaged("); - w.Write(argName); - w.Write(")"); - return; - } - w.Write(GetMarshallerFullName(w, sig)); - w.Write(".ConvertToManaged("); - w.Write(argName); - w.Write(")"); - } - - /// Returns the full marshaller name (e.g. global::ABI.Windows.Foundation.UriMarshaller). - /// When the marshaller would land in the writer's current ABI namespace, returns just the - /// short marshaller class name (e.g. BasicStructMarshaller) — mirrors C++ which - /// elides the qualifier in same-namespace contexts. - private static string GetMarshallerFullName(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - string ns = td.Type?.Namespace?.Value ?? string.Empty; - string name = td.Type?.Name?.Value ?? string.Empty; - // Apply mapped type remapping (e.g. System.Uri -> Windows.Foundation.Uri) - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - string nameStripped = Helpers.StripBackticks(name); - // If the writer is currently in the matching ABI namespace, drop the qualifier. - if (w.InAbiNamespace && string.Equals(w.CurrentNamespace, ns, System.StringComparison.Ordinal)) - { - return nameStripped + "Marshaller"; - } - return "global::ABI." + ns + "." + nameStripped + "Marshaller"; - } - return "global::ABI.Object.Marshaller"; - } - - private static string GetParamName(ParamInfo p, string? paramNameOverride) - { - string name = paramNameOverride ?? p.Parameter.Name ?? "param"; - return Helpers.IsKeyword(name) ? "@" + name : name; - } - - private static string GetParamLocalName(ParamInfo p, string? paramNameOverride) - { - // For local helper variables (e.g. __), strip the @ escape since `__event` is valid. - return paramNameOverride ?? p.Parameter.Name ?? "param"; - } - - private static bool IsString(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - return sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && - corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String; - } - - /// True if the type signature is System.Type (or a TypeRef/TypeSpec resolving to it, - /// or the WinRT Windows.UI.Xaml.Interop.TypeName struct that's mapped to it). - private static bool IsSystemType(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - string ns = td.Type?.Namespace?.Value ?? string.Empty; - string name = td.Type?.Name?.Value ?? string.Empty; - if (ns == "System" && name == "Type") { return true; } - // The WinMD source type for System.Type is Windows.UI.Xaml.Interop.TypeName. - if (ns == "Windows.UI.Xaml.Interop" && name == "TypeName") { return true; } - } - return false; - } - - /// Emits the conversion of a parameter from its projected (managed) form to the ABI argument form. - private static void EmitParamArgConversion(TypeWriter w, ParamInfo p, string? paramNameOverride = null) - { - string pname = paramNameOverride ?? p.Parameter.Name ?? "param"; - // bool: ABI is 'bool' directly; pass as-is. - if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib && - corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) - { - w.Write(pname); - } - // char: ABI is 'char' directly; pass as-is. - else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib2 && - corlib2.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) - { - w.Write(pname); - } - // Enums: function pointer signature uses the projected enum type, so pass directly. - else if (IsEnumType(p.Type)) - { - w.Write(pname); - } - else - { - w.Write(pname); - } - } - - /// True if the type is a blittable primitive (or enum) directly representable - /// at the ABI: bool/byte/sbyte/short/ushort/int/uint/long/ulong/float/double/char and enums. - private static bool IsBlittablePrimitive(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) - { - return corlib.ElementType is - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char; - } - // Enum (TypeDefOrRef-based value type with non-Object base) - same module or cross-module - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - if (td.Type is TypeDefinition def && TypeCategorization.GetCategory(def) == TypeCategory.Enum) - { - return true; - } - // Cross-module enum: try to resolve via the metadata cache. - if (td.Type is TypeReference tr && _cacheRef is not null) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - TypeDefinition? resolved = _cacheRef.Find(ns + "." + name); - if (resolved is not null && TypeCategorization.GetCategory(resolved) == TypeCategory.Enum) - { - return true; - } - } - } - return false; - } - - /// True for any struct type that can be passed directly across the WinRT ABI - /// (no per-field marshalling required). This includes blittable structs and "almost-blittable" - /// structs that have only primitive fields like bool/char (whose C# layout matches the WinRT ABI). - /// Excludes structs with reference type fields (string/object/runtime classes/etc.). - /// True for structs that have at least one reference type field (string, generic - /// instance Nullable<T>, etc.). These need per-field marshalling via the *Marshaller class - /// (ConvertToUnmanaged/ConvertToManaged/Dispose). - private static bool IsComplexStruct(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } - TypeDefinition? def = td.Type as TypeDefinition; - if (def is null && _cacheRef is not null && td.Type is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - if (ns == "System" && name == "Guid") { return false; } - def = _cacheRef.Find(ns + "." + name); - } - if (def is null) { return false; } - TypeCategory cat = TypeCategorization.GetCategory(def); - if (cat != TypeCategory.Struct) { return false; } - // Mirror C++ is_type_blittable: mapped struct types short-circuit based on - // RequiresMarshaling, regardless of inner field layout. So for mapped types like - // Duration, KeyTime, RepeatBehavior (RequiresMarshaling=false), they're never "complex". - { - string sNs = td.Type?.Namespace?.Value ?? string.Empty; - string sName = td.Type?.Name?.Value ?? string.Empty; - MappedType? sMapped = MappedTypes.Get(sNs, sName); - if (sMapped is not null) { return false; } - } - // A struct is "complex" if it has any field that is not a blittable primitive nor an - // almost-blittable struct (i.e. has a string/object/Nullable/etc. field). - foreach (FieldDefinition field in def.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - if (IsBlittablePrimitive(ft)) { continue; } - if (IsAnyStruct(ft)) { continue; } - return true; - } - return false; - } - - private static bool IsAnyStruct(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is not AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) { return false; } - TypeDefinition? def = td.Type as TypeDefinition; - if (def is null && _cacheRef is not null && td.Type is TypeReference trEarly) - { - string ns = trEarly.Namespace?.Value ?? string.Empty; - string name = trEarly.Name?.Value ?? string.Empty; - if (ns == "System" && name == "Guid") { return true; } - def = _cacheRef.Find(ns + "." + name); - } - if (def is null) { return false; } - // Special case: mapped struct types short-circuit based on RequiresMarshaling, mirroring - // C++ is_type_blittable: 'auto mapping = get_mapped_type(...); return !mapping->requires_marshaling'. - // Only applies to actual structs (not mapped interfaces like IAsyncAction). - if (TypeCategorization.GetCategory(def) == TypeCategory.Struct) - { - string sNs = td.Type?.Namespace?.Value ?? string.Empty; - string sName = td.Type?.Name?.Value ?? string.Empty; - MappedType? sMapped = MappedTypes.Get(sNs, sName); - if (sMapped is not null) { return !sMapped.RequiresMarshaling; } - } - TypeCategory cat = TypeCategorization.GetCategory(def); - if (cat != TypeCategory.Struct) { return false; } - // Reject if any instance field is a reference type (string/object/runtime class/etc.). - foreach (FieldDefinition field in def.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - AsmResolver.DotNet.Signatures.TypeSignature ft = field.Signature.FieldType; - if (ft is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibField) - { - if (corlibField.ElementType is - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String or - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object) - { return false; } - continue; - } - // Recurse: nested struct must also pass IsAnyStruct, otherwise reject. - if (IsBlittablePrimitive(ft)) { continue; } - if (IsAnyStruct(ft)) { continue; } - return false; - } - return true; - } - - /// Returns the ABI type name for a blittable struct (the projected type name). - private static string GetBlittableStructAbiType(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig) - { - // Mapped value types (DateTime/TimeSpan) use the ABI type, not the projected type. - if (IsMappedAbiValueType(sig)) { return GetMappedAbiTypeName(sig); } - return w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, sig, false))); - } - - /// Returns the ABI struct type name for a complex struct (e.g. global::ABI.Windows.Web.Http.HttpProgress). - /// When the writer is currently in the matching ABI namespace, returns just the - /// short type name (e.g. HttpProgress) to mirror the C++ tool which uses the - /// unqualified name in same-namespace contexts. - private static string GetAbiStructTypeName(TypeWriter w, AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - string ns = td.Type?.Namespace?.Value ?? string.Empty; - string name = td.Type?.Name?.Value ?? string.Empty; - // If this struct is mapped, use the mapped namespace+name (e.g. - // 'Windows.UI.Xaml.Interop.TypeName' is mapped to 'System.Type', so the ABI struct - // is 'global::ABI.System.Type', not 'global::ABI.Windows.UI.Xaml.Interop.TypeName'). - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - string nameStripped = Helpers.StripBackticks(name); - // If the writer is currently in the matching ABI namespace, drop the qualifier. - if (w.InAbiNamespace && string.Equals(w.CurrentNamespace, ns, System.StringComparison.Ordinal)) - { - return nameStripped; - } - return "global::ABI." + ns + "." + nameStripped; - } - return "global::ABI.Object"; - } - - private static string GetAbiPrimitiveType(AsmResolver.DotNet.Signatures.TypeSignature sig) - { - if (sig is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib) - { - return corlib.ElementType switch - { - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean => "bool", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char => "char", - _ => GetAbiFundamentalTypeFromCorLib(corlib.ElementType), - }; - } - // Enum: use the projected enum type as the ABI signature (truth pattern). - if (sig is AsmResolver.DotNet.Signatures.TypeDefOrRefSignature td) - { - TypeDefinition? def = td.Type as TypeDefinition; - if (def is null && _cacheRef is not null && td.Type is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - def = _cacheRef.Find(ns + "." + name); - } - if (def is not null && TypeCategorization.GetCategory(def) == TypeCategory.Enum) - { - return _cacheRef is null ? "int" : GetProjectedEnumName(def); - } - } - return "int"; - } - - private static string GetProjectedEnumName(TypeDefinition def) - { - string ns = def.Namespace?.Value ?? string.Empty; - string name = def.Name?.Value ?? string.Empty; - // Apply mapped-type translation so consumers see the projected (.NET) enum name - // (e.g. Windows.UI.Xaml.Interop.NotifyCollectionChangedAction → - // System.Collections.Specialized.NotifyCollectionChangedAction). Mirrors the same - // remapping that WriteTypedefName performs. - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - return string.IsNullOrEmpty(ns) ? "global::" + name : "global::" + ns + "." + name; - } - - private static string GetAbiFundamentalTypeFromCorLib(AsmResolver.PE.DotNet.Metadata.Tables.ElementType et) - { - return et switch - { - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => "sbyte", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => "byte", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => "short", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => "ushort", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => "int", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => "uint", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => "long", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => "ulong", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4 => "float", - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8 => "double", - _ => "int", - }; - } - - /// - /// Writes the IReference<T> implementation for a struct/enum/delegate - /// (mirrors C++ write_reference_impl). - /// - private static void WriteReferenceImpl(TypeWriter w, TypeDefinition type) - { - string name = type.Name?.Value ?? string.Empty; - string nameStripped = Helpers.StripBackticks(name); - string visibility = w.Settings.Component ? "public" : "file"; - bool blittable = IsTypeBlittable(type); - - w.Write("\n"); - w.Write(visibility); - w.Write(" static unsafe class "); - w.Write(nameStripped); - w.Write("ReferenceImpl\n{\n"); - w.Write(" [FixedAddressValueType]\n"); - w.Write(" private static readonly ReferenceVftbl Vftbl;\n\n"); - w.Write(" static "); - w.Write(nameStripped); - w.Write("ReferenceImpl()\n {\n"); - w.Write(" *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable;\n"); - w.Write(" Vftbl.get_Value = &get_Value;\n"); - w.Write(" }\n\n"); - w.Write(" public static nint Vtable\n {\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get => (nint)Unsafe.AsPointer(in Vftbl);\n }\n\n"); - w.Write(" [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])]\n"); - bool isBlittableStructType = blittable && TypeCategorization.GetCategory(type) == TypeCategory.Struct; - bool isNonBlittableStructType = !blittable && TypeCategorization.GetCategory(type) == TypeCategory.Struct; - if ((blittable && TypeCategorization.GetCategory(type) != TypeCategory.Struct) - || isBlittableStructType) - { - // For blittable types and blittable structs: direct memcpy via C# struct assignment. - // Even bool/char fields work because their managed layout (1 byte / 2 bytes) matches - // the WinRT ABI. - w.Write(" public static int get_Value(void* thisPtr, void* result)\n {\n"); - w.Write(" if (result is null)\n {\n"); - w.Write(" return unchecked((int)0x80004003);\n }\n\n"); - w.Write(" try\n {\n"); - w.Write(" var value = ("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(")(ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr));\n"); - w.Write(" *("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write("*)result = value;\n"); - w.Write(" return 0;\n }\n"); - w.Write(" catch (Exception e)\n {\n"); - w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e);\n }\n"); - w.Write(" }\n"); - } - else if (isNonBlittableStructType) - { - // Non-blittable struct: marshal via Marshaller.ConvertToUnmanaged then write the - // (ABI) struct value into the result pointer. Mirrors C++ write_reference_impl which - // emits 'unboxedValue = (T)...; value = TMarshaller.ConvertToUnmanaged(unboxedValue); - // *(ABIT*)result = value;'. - w.Write(" public static int get_Value(void* thisPtr, void* result)\n {\n"); - w.Write(" if (result is null)\n {\n"); - w.Write(" return unchecked((int)0x80004003);\n }\n\n"); - w.Write(" try\n {\n"); - w.Write(" "); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(" unboxedValue = ("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(")ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr);\n"); - w.Write(" "); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write(" value = "); - w.Write(nameStripped); - w.Write("Marshaller.ConvertToUnmanaged(unboxedValue);\n"); - w.Write(" *("); - WriteTypedefName(w, type, TypedefNameType.ABI, false); - w.Write("*)result = value;\n"); - w.Write(" return 0;\n }\n"); - w.Write(" catch (Exception e)\n {\n"); - w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e);\n }\n"); - w.Write(" }\n"); - } - else if (TypeCategorization.GetCategory(type) is TypeCategory.Class or TypeCategory.Delegate) - { - // Non-blittable runtime class / delegate: marshal via Marshaller and detach. - w.Write(" public static int get_Value(void* thisPtr, void* result)\n {\n"); - w.Write(" if (result is null)\n {\n"); - w.Write(" return unchecked((int)0x80004003);\n }\n\n"); - w.Write(" try\n {\n"); - w.Write(" "); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(" unboxedValue = ("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - w.Write(")ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr);\n"); - w.Write(" void* value = "); - // Use the same-namespace short marshaller name (we're in the ABI namespace). - w.Write(nameStripped); - w.Write("Marshaller.ConvertToUnmanaged(unboxedValue).DetachThisPtrUnsafe();\n"); - w.Write(" *(void**)result = value;\n"); - w.Write(" return 0;\n }\n"); - w.Write(" catch (Exception e)\n {\n"); - w.Write(" return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e);\n }\n"); - w.Write(" }\n"); - } - else - { - // Unreachable: WriteReferenceImpl is only called for enum/struct/delegate types - // (WriteAbiEnum / WriteAbiStruct / WriteAbiDelegate dispatchers). Enums are blittable - // (handled by the first branch), structs by the first/second branches, delegates by - // the third. Defensive: emit a runtime assertion in case a future caller dispatches - // for an unsupported category. - throw new System.InvalidOperationException( - $"WriteReferenceImpl: unsupported type category {TypeCategorization.GetCategory(type)} " + - $"for type '{type.FullName}'. Expected enum/struct/delegate."); - } - // IID property: matches C++ write_reference_impl, which appends a 'public static ref readonly Guid IID' - // property pointing at the reference type's IID (e.g. IID_Windows_AI_Actions_ActionEntityKindReference). - w.Write("\n public static ref readonly Guid IID\n {\n"); - w.Write(" [MethodImpl(MethodImplOptions.AggressiveInlining)]\n"); - w.Write(" get => ref global::ABI.InterfaceIIDs."); - WriteIidReferenceGuidPropertyName(w, type); - w.Write(";\n }\n"); - w.Write("}\n\n"); - } - - /// Mirrors C++ write_abi_type: writes the ABI type for a type semantics. - public static void WriteAbiType(TypeWriter w, TypeSemantics semantics) - { - switch (semantics) - { - case TypeSemantics.Fundamental f: - w.Write(GetAbiFundamentalType(f.Type)); - break; - case TypeSemantics.Object_: - w.Write("void*"); - break; - case TypeSemantics.Guid_: - w.Write("Guid"); - break; - case TypeSemantics.Type_: - w.Write("global::WindowsRuntime.InteropServices.WindowsRuntimeTypeName"); - break; - case TypeSemantics.Definition d: - if (TypeCategorization.GetCategory(d.Type) is TypeCategory.Enum) - { - // Enums in WinRT ABI use the projected enum type directly (since their C# - // layout matches their underlying integer ABI representation 1:1). - WriteTypedefName(w, d.Type, TypedefNameType.Projected, true); - } - else if (TypeCategorization.GetCategory(d.Type) is TypeCategory.Struct) - { - string dNs = d.Type.Namespace?.Value ?? string.Empty; - string dName = d.Type.Name?.Value ?? string.Empty; - // Special case: mapped value types that require ABI marshalling - // (DateTime/TimeSpan -> ABI.System.DateTimeOffset/TimeSpan). - if (dNs == "Windows.Foundation" && dName == "DateTime") - { - w.Write("global::ABI.System.DateTimeOffset"); - break; - } - if (dNs == "Windows.Foundation" && dName == "TimeSpan") - { - w.Write("global::ABI.System.TimeSpan"); - break; - } - if (dNs == "Windows.Foundation" && dName == "HResult") - { - w.Write("global::ABI.System.Exception"); - break; - } - if (dNs == "Windows.UI.Xaml.Interop" && dName == "TypeName") - { - // System.Type ABI struct: maps to global::ABI.System.Type, not the - // ABI.Windows.UI.Xaml.Interop.TypeName form. - w.Write("global::ABI.System.Type"); - break; - } - AsmResolver.DotNet.Signatures.TypeSignature dts = d.Type.ToTypeSignature(); - // "Almost-blittable" structs (with bool/char fields but no reference-type - // fields) can pass through using the projected type since the C# layout - // matches the WinRT ABI directly. Truly complex structs (with string/object/ - // Nullable fields) need the ABI struct. - if (IsAnyStruct(dts)) - { - WriteTypedefName(w, d.Type, TypedefNameType.Projected, true); - } - else - { - WriteTypedefName(w, d.Type, TypedefNameType.ABI, true); - } - } - else - { - w.Write("void*"); - } - break; - case TypeSemantics.Reference r: - // Cross-module typeref: try resolving the type, applying mapped-type translation - // for the field/parameter type after resolution. - if (_cacheRef is not null) - { - string rns = r.Reference_.Namespace?.Value ?? string.Empty; - string rname = r.Reference_.Name?.Value ?? string.Empty; - // Special case: mapped value types that require ABI marshalling. - if (rns == "Windows.Foundation" && rname == "DateTime") - { - w.Write("global::ABI.System.DateTimeOffset"); - break; - } - if (rns == "Windows.Foundation" && rname == "TimeSpan") - { - w.Write("global::ABI.System.TimeSpan"); - break; - } - if (rns == "Windows.Foundation" && rname == "HResult") - { - w.Write("global::ABI.System.Exception"); - break; - } - // Look up the type by its ORIGINAL (unmapped) name in the cache. - TypeDefinition? rd = _cacheRef.Find(rns + "." + rname); - // If not found, try the mapped name (for cases where the mapping target is in the cache). - if (rd is null) - { - MappedType? rmapped = MappedTypes.Get(rns, rname); - if (rmapped is not null) - { - rd = _cacheRef.Find(rmapped.MappedNamespace + "." + rmapped.MappedName); - } - } - if (rd is not null) - { - TypeCategory cat = TypeCategorization.GetCategory(rd); - if (cat == TypeCategory.Enum) - { - // Enums use the projected enum type directly (C# layout == ABI layout). - WriteTypedefName(w, rd, TypedefNameType.Projected, true); - break; - } - if (cat == TypeCategory.Struct) - { - // Special case: HResult is mapped to System.Exception (a reference type) - // but its ABI representation is the global::ABI.System.Exception struct - // (which wraps the underlying HRESULT int). - string rdNs = rd.Namespace?.Value ?? string.Empty; - string rdName = rd.Name?.Value ?? string.Empty; - if (rdNs == "Windows.Foundation" && rdName == "HResult") - { - w.Write("global::ABI.System.Exception"); - break; - } - if (IsAnyStruct(rd.ToTypeSignature())) - { - WriteTypedefName(w, rd, TypedefNameType.Projected, true); - } - else - { - WriteTypedefName(w, rd, TypedefNameType.ABI, true); - } - break; - } - } - } - // Unresolved cross-assembly TypeRef. If the signature was encoded as a value type - // (e.g. WindowId from Microsoft.UI.winmd when that winmd isn't loaded), assume it's - // a blittable struct and emit the projected type name — the consumer's compiler - // will resolve it via their own references. Otherwise (encoded as Class) emit - // void* (it's a runtime class/interface/delegate). - if (r.IsValueType) - { - string rns = r.Reference_.Namespace?.Value ?? string.Empty; - string rname = r.Reference_.Name?.Value ?? string.Empty; - w.Write("global::"); - if (!string.IsNullOrEmpty(rns)) { w.Write(rns); w.Write("."); } - w.Write(Helpers.StripBackticks(rname)); - break; - } - w.Write("void*"); - break; - case TypeSemantics.GenericInstance: - w.Write("void*"); - break; - default: - w.Write("void*"); - break; - } - } - - private static string GetAbiFundamentalType(FundamentalType t) => t switch - { - FundamentalType.Boolean => "bool", - FundamentalType.Char => "char", - FundamentalType.String => "void*", - _ => FundamentalTypes.ToCSharpType(t) - }; -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Class.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Class.cs deleted file mode 100644 index 9885fb6823..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Class.cs +++ /dev/null @@ -1,708 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Class emission helpers, mirroring functions in code_writers.h. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ is_fast_abi_class. - public static bool IsFastAbiClass(TypeDefinition type, Settings settings) - { - // Fast ABI is enabled when the type is marked [FastAbi] and netstandard_compat is off - // (CsWinRT 3.0 always has netstandard_compat = false, but we keep the gate for fidelity). - return !settings.NetstandardCompat && - TypeCategorization.HasAttribute(type, "Windows.Foundation.Metadata", "FastAbiAttribute"); - } - - /// Mirrors C++ write_class_modifiers. - public static void WriteClassModifiers(TypeWriter w, TypeDefinition type) - { - if (TypeCategorization.IsStatic(type)) - { - w.Write("static "); - return; - } - if (type.IsSealed) - { - w.Write("sealed "); - } - } - - /// - /// Returns the fast-abi class type for if the interface is - /// exclusive_to a class marked [FastAbi]; otherwise null. Mirrors C++ - /// find_fast_abi_class_type in helpers.h. - /// - public static TypeDefinition? FindFastAbiClassType(TypeDefinition iface) - { - if (_cacheRef is null) { return null; } - TypeDefinition? exclusiveToClass = GetExclusiveToType(iface); - if (exclusiveToClass is null) { return null; } - if (!IsFastAbiClass(exclusiveToClass, GetSettings(iface))) { return null; } - return exclusiveToClass; - } - - /// - /// Returns the fast-abi class info (class type + default interface + sorted other exclusive - /// interfaces) for , if the interface is exclusive_to a fast-abi - /// class; otherwise null. Mirrors C++ get_fast_abi_class_for_interface. - /// - public static (TypeDefinition Class, TypeDefinition? Default, System.Collections.Generic.List Others)? GetFastAbiClassForInterface(TypeDefinition iface) - { - TypeDefinition? cls = FindFastAbiClassType(iface); - if (cls is null) { return null; } - (TypeDefinition? def, System.Collections.Generic.List others) = GetFastAbiInterfaces(cls); - return (cls, def, others); - } - - /// - /// Whether is a non-default exclusive interface of a fast-abi class - /// (i.e. its members are merged into the default interface's vtable and dispatched through - /// the default interface's ABI Methods class). Mirrors C++ fast_abi_class::contains_other_interface. - /// - public static bool IsFastAbiOtherInterface(TypeDefinition iface) - { - var fastAbi = GetFastAbiClassForInterface(iface); - if (fastAbi is null) { return false; } - if (fastAbi.Value.Default is not null && InterfacesEqual(fastAbi.Value.Default, iface)) { return false; } - foreach (TypeDefinition other in fastAbi.Value.Others) - { - if (InterfacesEqual(other, iface)) { return true; } - } - return false; - } - - /// - /// Returns true if is the default interface of a fast-abi class. - /// - public static bool IsFastAbiDefaultInterface(TypeDefinition iface) - { - var fastAbi = GetFastAbiClassForInterface(iface); - if (fastAbi is null) { return false; } - return fastAbi.Value.Default is not null && InterfacesEqual(fastAbi.Value.Default, iface); - } - - private static bool InterfacesEqual(TypeDefinition a, TypeDefinition b) - { - if (a == b) { return true; } - return (a.Namespace?.Value ?? string.Empty) == (b.Namespace?.Value ?? string.Empty) - && (a.Name?.Value ?? string.Empty) == (b.Name?.Value ?? string.Empty); - } - - // We don't have direct access to the active Settings from a static helper that only takes - // a TypeDefinition. The fast-abi flag is purely determined by the [FastAbiAttribute] (the - // netstandard_compat gate is always false in CsWinRT 3.0). Pass an empty Settings stand-in - // so the IsFastAbiClass check evaluates only the attribute presence. - private static Settings GetSettings(TypeDefinition _) => s_emptySettingsForFastAbi; - private static readonly Settings s_emptySettingsForFastAbi = new() { NetstandardCompat = false }; - - /// - /// Returns the [Default] interface and the [ExclusiveTo] interfaces (sorted) for fast ABI. - /// Mirrors C++ get_default_and_exclusive_interfaces + sort_fast_abi_ifaces. - /// - public static (TypeDefinition? DefaultInterface, System.Collections.Generic.List OtherInterfaces) GetFastAbiInterfaces(TypeDefinition classType) - { - TypeDefinition? defaultIface = null; - System.Collections.Generic.List exclusiveIfaces = new(); - foreach (InterfaceImplementation impl in classType.Interfaces) - { - if (impl.Interface is null) { continue; } - TypeDefinition? ifaceTd = impl.Interface as TypeDefinition; - if (ifaceTd is null && _cacheRef is not null) - { - try { ifaceTd = impl.Interface.Resolve(_cacheRef.RuntimeContext); } - catch { ifaceTd = null; } - } - if (ifaceTd is null) { continue; } - - if (Helpers.IsDefaultInterface(impl)) - { - defaultIface = ifaceTd; - } - else if (TypeCategorization.IsExclusiveTo(ifaceTd)) - { - exclusiveIfaces.Add(ifaceTd); - } - } - // Sort exclusive interfaces by: - // 1. Number of [PreviousContractVersion] attrs (ascending; newer interfaces have more) - // 2. Contract version (ascending) - // 3. Type version (ascending) - // 4. Type namespace and name (ascending) - exclusiveIfaces.Sort((a, b) => - { - int aPrev = -CountAttributes(a, "Windows.Foundation.Metadata", "PreviousContractVersionAttribute"); - int bPrev = -CountAttributes(b, "Windows.Foundation.Metadata", "PreviousContractVersionAttribute"); - if (aPrev != bPrev) { return aPrev.CompareTo(bPrev); } - - int? aCV = Helpers.GetContractVersion(a); - int? bCV = Helpers.GetContractVersion(b); - if (aCV.HasValue && bCV.HasValue && aCV.Value != bCV.Value) { return aCV.Value.CompareTo(bCV.Value); } - - int? aV = Helpers.GetVersion(a); - int? bV = Helpers.GetVersion(b); - if (aV.HasValue && bV.HasValue && aV.Value != bV.Value) { return aV.Value.CompareTo(bV.Value); } - - string aNs = a.Namespace?.Value ?? string.Empty; - string bNs = b.Namespace?.Value ?? string.Empty; - if (aNs != bNs) { return System.StringComparer.Ordinal.Compare(aNs, bNs); } - return System.StringComparer.Ordinal.Compare(a.Name?.Value ?? string.Empty, b.Name?.Value ?? string.Empty); - }); - return (defaultIface, exclusiveIfaces); - } - - private static int CountAttributes(IHasCustomAttribute member, string ns, string name) - { - int count = 0; - for (int i = 0; i < member.CustomAttributes.Count; i++) - { - CustomAttribute attr = member.CustomAttributes[i]; - ITypeDefOrRef? type = attr.Constructor?.DeclaringType; - if (type is not null && type.Namespace == ns && type.Name == name) { count++; } - } - return count; - } - - /// Mirrors C++ get_gc_pressure_amount. - public static int GetGcPressureAmount(TypeDefinition type) - { - if (!type.IsSealed) { return 0; } - CustomAttribute? attr = TypeCategorization.GetAttribute(type, "Windows.Foundation.Metadata", "GCPressureAttribute"); - if (attr is null || attr.Signature is null) { return 0; } - // The attribute has a single named arg "Amount" of an enum type. Defaults: 0=Low, 1=Medium, 2=High. - // We try both fixed args and named args. - int amount = -1; - if (attr.Signature.NamedArguments.Count > 0) - { - object? v = attr.Signature.NamedArguments[0].Argument.Element; - if (v is int i) { amount = i; } - } - return amount switch - { - 0 => 12000, - 1 => 120000, - 2 => 1200000, - _ => 0 - }; - } - - /// - /// Mirrors C++ write_static_class. - /// - public static void WriteStaticClass(TypeWriter w, TypeDefinition type) - { - bool prevCheckPlatform = w.CheckPlatform; - string prevPlatform = w.Platform; - w.CheckPlatform = true; - w.Platform = string.Empty; - try - { - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteTypeCustomAttributes(w, type, true); - w.Write(Helpers.InternalAccessibility(w.Settings)); - w.Write(" static class "); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write("\n{\n"); - WriteStaticClassMembers(w, type); - w.Write("}\n"); - } - finally - { - w.CheckPlatform = prevCheckPlatform; - w.Platform = prevPlatform; - } - } - - /// - /// Emits static members from [Static] factory interfaces. Mirrors C++ write_static_members. - /// - public static void WriteStaticClassMembers(TypeWriter w, TypeDefinition type) - { - if (_cacheRef is null) { return; } - // Per-property accessor state (origin tracking for getter/setter) - Dictionary properties = new(System.StringComparer.Ordinal); - // Track the static factory ifaces we've emitted objref fields for (to dedupe) - HashSet emittedObjRefs = new(System.StringComparer.Ordinal); - - string runtimeClassFullName = (type.Namespace?.Value ?? string.Empty) + "." + (type.Name?.Value ?? string.Empty); - - foreach (KeyValuePair kv in AttributedTypes.Get(type, _cacheRef)) - { - AttributedType factory = kv.Value; - if (!(factory.Statics && factory.Type is not null)) { continue; } - TypeDefinition staticIface = factory.Type; - - // Compute the objref name for this static factory interface. - string objRef = GetObjRefName(w, staticIface); - // Compute the ABI Methods static class name (e.g. "global::ABI.Windows.System.ILauncherStaticsMethods") - string abiClass = w.WriteTemp("%", new System.Action(_ => - { - WriteTypedefName(w, staticIface, TypedefNameType.StaticAbiClass, true); - })); - if (!abiClass.StartsWith("global::", System.StringComparison.Ordinal)) - { - abiClass = "global::" + abiClass; - } - - // Emit the lazy static objref field (mirrors truth's pattern) once per static iface. - if (emittedObjRefs.Add(objRef)) - { - WriteStaticFactoryObjRef(w, staticIface, runtimeClassFullName, objRef); - } - - // Compute the platform attribute string from the static factory interface's - // [ContractVersion] attribute. Mirrors C++ code_writers.h:3315 - // 'auto platform_attribute = write_platform_attribute_temp(w, factory.type);' - // and the per-static-method/event/property emission at lines 3316-3349. - string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, staticIface))); - - // Methods - foreach (MethodDefinition method in staticIface.Methods) - { - if (Helpers.IsSpecial(method)) { continue; } - MethodSig sig = new(method); - string mname = method.Name?.Value ?? string.Empty; - w.Write("\n"); - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write("public static "); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(mname); - w.Write("("); - WriteParameterList(w, sig); - if (w.Settings.ReferenceProjection) - { - // Mirrors C++ write_abi_static_method_call (code_writers.h:1637): static - // method bodies become 'throw null' in reference projection mode. - w.Write(") => throw null;\n"); - } - else - { - w.Write(") => "); - w.Write(abiClass); - w.Write("."); - w.Write(mname); - w.Write("("); - w.Write(objRef); - for (int i = 0; i < sig.Params.Count; i++) - { - w.Write(", "); - WriteParameterNameWithModifier(w, sig.Params[i]); - } - w.Write(");\n"); - } - } - // Events: dispatch via static ABI class which returns an event source. - foreach (EventDefinition evt in staticIface.Events) - { - string evtName = evt.Name?.Value ?? string.Empty; - w.Write("\n"); - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write("public static event "); - WriteEventType(w, evt); - w.Write(" "); - w.Write(evtName); - w.Write("\n{\n"); - if (w.Settings.ReferenceProjection) - { - // Mirrors C++ write_abi_event_source_static_method_call (code_writers.h:1711): - // event accessor bodies become 'throw null' in reference projection mode. - w.Write(" add => throw null;\n"); - w.Write(" remove => throw null;\n"); - } - else - { - w.Write(" add => "); - w.Write(abiClass); - w.Write("."); - w.Write(evtName); - w.Write("("); - w.Write(objRef); - w.Write(", "); - w.Write(objRef); - w.Write(").Subscribe(value);\n"); - w.Write(" remove => "); - w.Write(abiClass); - w.Write("."); - w.Write(evtName); - w.Write("("); - w.Write(objRef); - w.Write(", "); - w.Write(objRef); - w.Write(").Unsubscribe(value);\n"); - } - w.Write("}\n"); - } - // Properties (merge getter/setter across interfaces, tracking origin per accessor) - foreach (PropertyDefinition prop in staticIface.Properties) - { - string propName = prop.Name?.Value ?? string.Empty; - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - string propType = WritePropType(w, prop); - if (!properties.TryGetValue(propName, out StaticPropertyAccessorState? state)) - { - state = new StaticPropertyAccessorState - { - PropTypeText = propType, - }; - properties[propName] = state; - } - if (getter is not null && !state.HasGetter) - { - state.HasGetter = true; - state.GetterAbiClass = abiClass; - state.GetterObjRef = objRef; - // Mirror C++ getter_platform tracking (code_writers.h:3328, 3342). - state.GetterPlatformAttribute = platformAttribute; - } - if (setter is not null && !state.HasSetter) - { - state.HasSetter = true; - state.SetterAbiClass = abiClass; - state.SetterObjRef = objRef; - // Mirror C++ setter_platform tracking (code_writers.h:3330, 3349). - state.SetterPlatformAttribute = platformAttribute; - } - } - } - - // Emit properties with merged accessors - foreach (KeyValuePair kv in properties) - { - StaticPropertyAccessorState s = kv.Value; - w.Write("\n"); - // Mirrors C++ code_writers.h:2041-2046: collapse to property-level platform attribute - // when getter and setter platforms match; otherwise emit per-accessor. - string getterPlat = s.GetterPlatformAttribute; - string setterPlat = s.SetterPlatformAttribute; - string propertyPlat = string.Empty; - bool bothSidesPresent = s.HasGetter && s.HasSetter; - if (!bothSidesPresent || getterPlat == setterPlat) - { - propertyPlat = !string.IsNullOrEmpty(getterPlat) ? getterPlat : setterPlat; - getterPlat = string.Empty; - setterPlat = string.Empty; - } - if (!string.IsNullOrEmpty(propertyPlat)) { w.Write(propertyPlat); } - w.Write("public static "); - w.Write(s.PropTypeText); - w.Write(" "); - w.Write(kv.Key); - // Getter-only -> expression body; otherwise -> accessor block (matches truth). - // In ref mode, all accessor bodies emit '=> throw null;' (mirrors C++ - // write_abi_get/set_property_static_method_call, code_writers.h:1669, 1683). - bool getterOnly = s.HasGetter && !s.HasSetter; - if (getterOnly) - { - if (w.Settings.ReferenceProjection) - { - w.Write(" => throw null;\n"); - } - else - { - w.Write(" => "); - w.Write(s.GetterAbiClass); - w.Write("."); - w.Write(kv.Key); - w.Write("("); - w.Write(s.GetterObjRef); - w.Write(");\n"); - } - } - else - { - w.Write("\n{\n"); - if (s.HasGetter) - { - if (!string.IsNullOrEmpty(getterPlat)) { w.Write(getterPlat); } - if (w.Settings.ReferenceProjection) - { - w.Write("get => throw null;\n"); - } - else - { - w.Write("get => "); - w.Write(s.GetterAbiClass); - w.Write("."); - w.Write(kv.Key); - w.Write("("); - w.Write(s.GetterObjRef); - w.Write(");\n"); - } - } - if (s.HasSetter) - { - if (!string.IsNullOrEmpty(setterPlat)) { w.Write(setterPlat); } - if (w.Settings.ReferenceProjection) - { - w.Write("set => throw null;\n"); - } - else - { - w.Write("set => "); - w.Write(s.SetterAbiClass); - w.Write("."); - w.Write(kv.Key); - w.Write("("); - w.Write(s.SetterObjRef); - w.Write(", value);\n"); - } - } - w.Write("}\n"); - } - } - } - - private sealed class StaticPropertyAccessorState - { - public bool HasGetter; - public bool HasSetter; - public string PropTypeText = string.Empty; - public string GetterAbiClass = string.Empty; - public string GetterObjRef = string.Empty; - public string SetterAbiClass = string.Empty; - public string SetterObjRef = string.Empty; - // Per-accessor platform attribute strings. Mirrors C++ getter_platform/setter_platform - // tracking in code_writers.h:3328-3349. - public string GetterPlatformAttribute = string.Empty; - public string SetterPlatformAttribute = string.Empty; - } - - /// - /// Emits the static lazy objref property for a static factory interface (mirrors truth's - /// pattern: lazy WindowsRuntimeObjectReference.GetActivationFactory(...)). - /// - private static void WriteStaticFactoryObjRef(TypeWriter w, TypeDefinition staticIface, string runtimeClassFullName, string objRefName) - { - w.Write("\nprivate static WindowsRuntimeObjectReference "); - w.Write(objRefName); - w.Write("\n{\n"); - if (w.Settings.ReferenceProjection) - { - // Mirrors C++ write_static_objref_definition (code_writers.h:2789): in ref mode - // the static factory objref getter body is just 'throw null;'. - w.Write(" get\n {\n throw null;\n }\n}\n"); - return; - } - w.Write(" get\n {\n"); - w.Write(" var __"); - w.Write(objRefName); - w.Write(" = field;\n"); - w.Write(" if (__"); - w.Write(objRefName); - w.Write(" != null && __"); - w.Write(objRefName); - w.Write(".IsInCurrentContext)\n {\n"); - w.Write(" return __"); - w.Write(objRefName); - w.Write(";\n }\n"); - w.Write(" return field = WindowsRuntimeObjectReference.GetActivationFactory(\""); - w.Write(runtimeClassFullName); - w.Write("\", "); - WriteIidExpression(w, staticIface); - w.Write(");\n }\n}\n"); - } - - /// - /// Mirrors C++ write_class. Emits a runtime class projection. - /// - public static void WriteClass(TypeWriter w, TypeDefinition type) - { - if (w.Settings.Component) { return; } - - if (TypeCategorization.IsStatic(type)) - { - WriteStaticClass(w, type); - return; - } - - // Mirror C++ writer::write_platform_guard set at the start of write_class. - // Tracks the highest platform seen within this class to suppress redundant - // [SupportedOSPlatform(...)] emissions across interface boundaries. - bool prevCheckPlatform = w.CheckPlatform; - string prevPlatform = w.Platform; - w.CheckPlatform = true; - w.Platform = string.Empty; - try - { - WriteClassCore(w, type); - } - finally - { - w.CheckPlatform = prevCheckPlatform; - w.Platform = prevPlatform; - } - } - - private static void WriteClassCore(TypeWriter w, TypeDefinition type) - { - string typeName = type.Name?.Value ?? string.Empty; - int gcPressure = GetGcPressureAmount(type); - - // Header attributes - w.Write("\n"); - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteTypeCustomAttributes(w, type, true); - WriteComWrapperMarshallerAttribute(w, type); - w.Write(w.Settings.Internal ? "internal" : "public"); - w.Write(" "); - WriteClassModifiers(w, type); - // Mirrors C++ write_class which uses '%class' (no partial) — runtime classes - // are emitted as plain (non-partial) classes. - w.Write("class "); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - WriteTypeInheritance(w, type, false, true); - w.Write("\n{\n"); - - // ObjRef field definitions for each implemented interface (mirrors C++ write_class_objrefs_definition). - // These back the per-interface dispatch in instance methods/properties and the - // IWindowsRuntimeInterface.GetInterface() implementations. - WriteClassObjRefDefinitions(w, type); - - // Constructor: WindowsRuntimeObjectReference-based constructor (RCW-like) - if (!w.Settings.ReferenceProjection) - { - string ctorAccess = type.IsSealed ? "internal" : "protected internal"; - w.Write("\n"); - w.Write(ctorAccess); - w.Write(" "); - w.Write(typeName); - w.Write("(WindowsRuntimeObjectReference nativeObjectReference)\n: base(nativeObjectReference)\n{\n"); - if (!type.IsSealed) - { - // For unsealed classes, the default interface objref needs to be initialized only - // when GetType() matches the projected class exactly (derived classes have their own - // default interface). The init; accessor on _objRef_ allows this set. - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); - if (defaultIface is not null) - { - string defaultObjRefName = GetObjRefName(w, defaultIface); - w.Write("if (GetType() == typeof("); - w.Write(typeName); - w.Write("))\n{\n"); - w.Write(defaultObjRefName); - w.Write(" = NativeObjectReference;\n"); - w.Write("}\n"); - } - } - if (gcPressure > 0) - { - w.Write("GC.AddMemoryPressure("); - w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(");\n"); - } - w.Write("}\n"); - } - else if (_cacheRef is not null) - { - // In ref mode, if WriteAttributedTypes will not emit any public constructors, - // we need a 'private TypeName() { throw null; }' to suppress the C# compiler's - // implicit public default constructor (which would expose an unintended API). - // Mirrors C++ code_writers.h:9519-9538 exactly: a type has constructors when - // either: - // - factory.activatable is true (parameterless or parameterized — Activatable - // always emits at least one ctor), OR - // - factory.composable && factory.type && factory.type.MethodList().size() > 0 - // (composable factories with NO methods don't emit any ctors). - bool hasRefModeCtors = false; - foreach (KeyValuePair kv in AttributedTypes.Get(type, _cacheRef)) - { - AttributedType factory = kv.Value; - if (factory.Activatable) - { - hasRefModeCtors = true; - break; - } - if (factory.Composable && factory.Type is not null && factory.Type.Methods.Count > 0) - { - hasRefModeCtors = true; - break; - } - } - if (!hasRefModeCtors) - { - EmitSyntheticPrivateCtor(w, typeName); - } - } - - // Activator/composer constructors from [Activatable]/[Composable] factory interfaces. - // Mirror C++ write_attributed_types: emits factory ctors AND static members (via - // write_static_members) BEFORE the override hooks and instance members. - WriteAttributedTypes(w, type); - - // Static members from [Static] factory interfaces (e.g. GetForCurrentView). - // C++ emits these inside write_attributed_types -> write_static_members; emit them - // here right after to preserve the same overall ordering. - WriteStaticClassMembers(w, type); - - // Conditional finalizer - if (gcPressure > 0) - { - w.Write("~"); - w.Write(typeName); - w.Write("()\n{\nGC.RemoveMemoryPressure("); - w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(");\n}\n"); - } - - // Class members from interfaces (instance methods, properties, events) - // Override hooks must be emitted BEFORE the public members to match the C++ - // ordering (write_class line 9591/9600/9601: hooks first, then write_class_members). - // HasUnwrappableNativeObjectReference and IsOverridableInterface overrides. - if (!w.Settings.ReferenceProjection) - { - w.Write("\nprotected override bool HasUnwrappableNativeObjectReference => "); - if (!type.IsSealed) - { - w.Write("GetType() == typeof("); - w.Write(typeName); - w.Write(");"); - } - else - { - w.Write("true;"); - } - w.Write("\n"); - - // IsOverridableInterface override (mirrors C++ write_custom_query_interface_impl). - // Emit '|| == iid' for each [Overridable] interface impl, then '|| base.IsOverridableInterface(in iid)' - // if the type has a base class, finally fall back to 'false' if no entries. - w.Write("\nprotected override bool IsOverridableInterface(in Guid iid) => "); - bool firstClause = true; - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (!Helpers.IsOverridable(impl)) { continue; } - ITypeDefOrRef? implRef = impl.Interface; - if (implRef is null) { continue; } - if (!firstClause) { w.Write(" || "); } - firstClause = false; - WriteIidExpression(w, implRef); - w.Write(" == iid"); - } - // base call when type has a non-object base class - bool hasBaseClass = type.BaseType is not null - && !(type.BaseType.Namespace?.Value == "System" && type.BaseType.Name?.Value == "Object") - && !(type.BaseType.Namespace?.Value == "WindowsRuntime" && type.BaseType.Name?.Value == "WindowsRuntimeObject"); - if (hasBaseClass) - { - if (!firstClause) { w.Write(" || "); } - w.Write("base.IsOverridableInterface(in iid)"); - firstClause = false; - } - if (firstClause) { w.Write("false"); } - w.Write(";\n"); - } - - WriteClassMembers(w, type); - - w.Write("}\n"); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ClassMembers.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ClassMembers.cs deleted file mode 100644 index f7b9af76fb..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ClassMembers.cs +++ /dev/null @@ -1,995 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Class member emission: walks implemented interfaces and emits the public/protected -/// instance methods, properties, and events (mirrors C++ write_class_members). -/// -internal static partial class CodeWriters -{ - /// - /// Emits all instance members (methods, properties, events) inherited from implemented interfaces. - /// Mirrors C++ write_class_members. In ref-projection mode, this is still called: type - /// declarations and per-interface objref getters are emitted, but non-mapped instance - /// method/property/event bodies are emitted as => throw null; stubs. - /// - public static void WriteClassMembers(TypeWriter w, TypeDefinition type) - { - HashSet writtenMethods = new(System.StringComparer.Ordinal); - // For properties: track per-name accessor presence so we can merge get/set across interfaces. - // Use insertion-order Dictionary so the per-class property emission order matches the - // .winmd metadata definition order (mirrors C++ which uses type.PropertyList() order). - Dictionary propertyState = new(System.StringComparer.Ordinal); - HashSet writtenEvents = new(System.StringComparer.Ordinal); - HashSet writtenInterfaces = new(); - - // Mirror C++ class member ordering: emit GetInterface()/GetDefaultInterface() per - // interface inside WriteInterfaceMembersRecursive (right before that interface's - // members), instead of one upfront block. This interleaves the GetInterface() impls - // with their corresponding interface body, matching truth's per-interface layout. - WriteInterfaceMembersRecursive(w, type, type, null, writtenMethods, propertyState, writtenEvents, writtenInterfaces); - - // After collecting all properties (with merged accessors), emit them. - foreach (KeyValuePair kvp in propertyState) - { - PropertyAccessorState s = kvp.Value; - // For generic-interface properties, emit the UnsafeAccessor static externs above the - // property declaration. Note: getter and setter use the same accessor name (because - // C# allows method overloading on parameter list for the static externs). - if (s.HasGetter && s.GetterIsGeneric && !string.IsNullOrEmpty(s.GetterGenericInteropType)) - { - w.Write("\n[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); - w.Write(kvp.Key); - w.Write("\")]\n"); - w.Write("static extern "); - w.Write(s.GetterPropTypeText); - w.Write(" "); - w.Write(s.GetterGenericAccessorName); - w.Write("([UnsafeAccessorType(\""); - w.Write(s.GetterGenericInteropType); - w.Write("\")] object _, WindowsRuntimeObjectReference thisReference);\n"); - } - if (s.HasSetter && s.SetterIsGeneric && !string.IsNullOrEmpty(s.SetterGenericInteropType)) - { - w.Write("\n[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); - w.Write(kvp.Key); - w.Write("\")]\n"); - w.Write("static extern void "); - w.Write(s.SetterGenericAccessorName); - w.Write("([UnsafeAccessorType(\""); - w.Write(s.SetterGenericInteropType); - w.Write("\")] object _, WindowsRuntimeObjectReference thisReference, "); - w.Write(s.SetterPropTypeText); - w.Write(" value);\n"); - } - - w.Write("\n"); - // Mirrors C++ code_writers.h:2041-2046: collapse to property-level platform attribute - // when getter and setter platforms match; otherwise emit per-accessor. - string getterPlat = s.GetterPlatformAttribute; - string setterPlat = s.SetterPlatformAttribute; - string propertyPlat = string.Empty; - // C++: if (getter_platform == setter_platform) { property_platform = getter_platform; getter_platform = ""; setter_platform = ""; } - // For getter-only or setter-only properties, only one side is set; compare the relevant side. - bool bothSidesPresent = s.HasGetter && s.HasSetter; - if (!bothSidesPresent || getterPlat == setterPlat) - { - // Collapse: prefer the populated side (matches C++ which compares string_view equality - // including both being empty). - propertyPlat = !string.IsNullOrEmpty(getterPlat) ? getterPlat : setterPlat; - getterPlat = string.Empty; - setterPlat = string.Empty; - } - if (!string.IsNullOrEmpty(propertyPlat)) { w.Write(propertyPlat); } - w.Write(s.Access); - w.Write(s.MethodSpec); - w.Write(s.PropTypeText); - w.Write(" "); - w.Write(kvp.Key); - // For getter-only properties, emit expression body: 'public T Prop => Expr;' - // For getter+setter or setter-only, use accessor block: 'public T Prop { get => ...; set => ...; }' - // (mirrors C++ which uses '%' template substitution where get-only collapses to '=> %'). - // - // In ref mode, all property bodies emit '=> throw null;' (mirrors C++ - // write_abi_get/set_property_static_method_call + write_unsafe_accessor_property_static_method_call, - // code_writers.h:1669, 1683, 1697). - bool getterOnly = s.HasGetter && !s.HasSetter; - if (getterOnly) - { - w.Write(" => "); - if (w.Settings.ReferenceProjection) - { - w.Write("throw null;"); - } - else if (s.GetterIsGeneric) - { - if (!string.IsNullOrEmpty(s.GetterGenericInteropType)) - { - w.Write(s.GetterGenericAccessorName); - w.Write("(null, "); - w.Write(s.GetterObjRef); - w.Write(");"); - } - else - { - w.Write("throw null!;"); - } - } - else - { - w.Write(s.GetterAbiClass); - w.Write("."); - w.Write(kvp.Key); - w.Write("("); - w.Write(s.GetterObjRef); - w.Write(");"); - } - w.Write("\n"); - } - else - { - w.Write("\n{\n"); - if (s.HasGetter) - { - if (!string.IsNullOrEmpty(getterPlat)) - { - w.Write(" "); - w.Write(getterPlat); - } - if (w.Settings.ReferenceProjection) - { - w.Write(" get => throw null;\n"); - } - else if (s.GetterIsGeneric) - { - if (!string.IsNullOrEmpty(s.GetterGenericInteropType)) - { - w.Write(" get => "); - w.Write(s.GetterGenericAccessorName); - w.Write("(null, "); - w.Write(s.GetterObjRef); - w.Write(");\n"); - } - else - { - w.Write(" get => throw null!;\n"); - } - } - else - { - w.Write(" get => "); - w.Write(s.GetterAbiClass); - w.Write("."); - w.Write(kvp.Key); - w.Write("("); - w.Write(s.GetterObjRef); - w.Write(");\n"); - } - } - if (s.HasSetter) - { - if (!string.IsNullOrEmpty(setterPlat)) - { - w.Write(" "); - w.Write(setterPlat); - } - if (w.Settings.ReferenceProjection) - { - w.Write(" set => throw null;\n"); - } - else if (s.SetterIsGeneric) - { - if (!string.IsNullOrEmpty(s.SetterGenericInteropType)) - { - w.Write(" set => "); - w.Write(s.SetterGenericAccessorName); - w.Write("(null, "); - w.Write(s.SetterObjRef); - w.Write(", value);\n"); - } - else - { - w.Write(" set => throw null!;\n"); - } - } - else - { - w.Write(" set => "); - w.Write(s.SetterAbiClass); - w.Write("."); - w.Write(kvp.Key); - w.Write("("); - w.Write(s.SetterObjRef); - w.Write(", value);\n"); - } - } - w.Write("}\n"); - } - - // For overridable properties, emit an explicit interface implementation that - // delegates to the protected property. Mirrors truth pattern: - // T InterfaceName.PropName { get => PropName; } - // T InterfaceName.PropName { set => PropName = value; } - if (s.IsOverridable && s.OverridableInterface is not null) - { - w.Write(s.PropTypeText); - w.Write(" "); - WriteInterfaceTypeNameForCcw(w, s.OverridableInterface); - w.Write("."); - w.Write(kvp.Key); - w.Write(" {"); - if (s.HasGetter) - { - w.Write("get => "); - w.Write(kvp.Key); - w.Write("; "); - } - if (s.HasSetter) - { - w.Write("set => "); - w.Write(kvp.Key); - w.Write(" = value; "); - } - w.Write("}\n"); - } - } - - // GetInterface() / GetDefaultInterface() impls are emitted per-interface inside - // WriteInterfaceMembersRecursive (matches the C++ tool's per-interface ordering). - } - - private static string BuildMethodSignatureKey(string name, MethodSig sig) - { - System.Text.StringBuilder sb = new(); - sb.Append(name); - sb.Append('('); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { sb.Append(','); } - sb.Append(sig.Params[i].Type?.FullName ?? "?"); - } - sb.Append(')'); - return sb.ToString(); - } - - private sealed class PropertyAccessorState - { - public bool HasGetter; - public bool HasSetter; - public string PropTypeText = string.Empty; - public string Access = "public "; - public string MethodSpec = string.Empty; - public string GetterAbiClass = string.Empty; - public string GetterObjRef = string.Empty; - public string SetterAbiClass = string.Empty; - public string SetterObjRef = string.Empty; - public string Name = string.Empty; - public bool GetterIsGeneric; - public bool SetterIsGeneric; - public string GetterGenericInteropType = string.Empty; - public string GetterGenericAccessorName = string.Empty; - public string GetterPropTypeText = string.Empty; - public string SetterGenericInteropType = string.Empty; - public string SetterGenericAccessorName = string.Empty; - public string SetterPropTypeText = string.Empty; - // True if this property comes from an Overridable interface (needs explicit interface impl). - public bool IsOverridable; - // The originating interface (used to qualify the explicit interface impl). - public ITypeDefOrRef? OverridableInterface; - // Per-accessor platform attribute strings from the originating interface's [ContractVersion], - // emitted before the property in ref mode. Mirrors C++ getter_platform/setter_platform - // tracking in code_writers.h:4306-4308 / 4323/4330. When both match, emit at the property - // level only; when they differ (getter and setter come from different interfaces with - // different platforms), emit per-accessor. - public string GetterPlatformAttribute = string.Empty; - public string SetterPlatformAttribute = string.Empty; - } - - /// - /// Returns true if the given interface implementation should appear in the class's inheritance list - /// (i.e., it has [Overridable], or is not [ExclusiveTo], or includeExclusiveInterface is set). - /// - private static bool IsInterfaceInInheritanceList(InterfaceImplementation impl, bool includeExclusiveInterface) - { - if (impl.Interface is null) { return false; } - if (Helpers.IsOverridable(impl)) { return true; } - if (includeExclusiveInterface) { return true; } - TypeDefinition? td = ResolveInterface(impl.Interface); - if (td is null) { return true; } - return !TypeCategorization.IsExclusiveTo(td); - } - - private static void WriteInterfaceMembersRecursive(TypeWriter w, TypeDefinition classType, TypeDefinition declaringType, - AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? currentInstance, - HashSet writtenMethods, IDictionary propertyState, HashSet writtenEvents, HashSet writtenInterfaces) - { - AsmResolver.DotNet.Signatures.GenericContext genCtx = new(currentInstance, null); - - foreach (InterfaceImplementation impl in declaringType.Interfaces) - { - if (impl.Interface is null) { continue; } - - // Resolve TypeRef to TypeDef using our cache - TypeDefinition? ifaceType = ResolveInterface(impl.Interface); - if (ifaceType is null) { continue; } - - if (writtenInterfaces.Contains(ifaceType)) { continue; } - _ = writtenInterfaces.Add(ifaceType); - - bool isOverridable = Helpers.IsOverridable(impl); - bool isProtected = TypeCategorization.HasAttribute(impl, "Windows.Foundation.Metadata", "ProtectedAttribute"); - - // Substitute generic type arguments using the current generic context BEFORE emitting - // any references to this interface. This is critical for nested recursion: e.g. when - // emitting members for IObservableMap's base IMap, we need to - // substitute !0/!1 with string/object so the generated code references - // IDictionary instead of IDictionary. Mirrors the C++ tool's - // writer.push_generic_args() stack inside for_typedef(). - ITypeDefOrRef substitutedInterface = impl.Interface; - AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? nextInstance = null; - if (impl.Interface is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - if (currentInstance is not null) - { - AsmResolver.DotNet.Signatures.TypeSignature subSig = gi.InstantiateGenericTypes(genCtx); - if (subSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature subGi) - { - nextInstance = subGi; - AsmResolver.DotNet.ITypeDefOrRef? newRef = subGi.ToTypeDefOrRef(); - if (newRef is not null) { substitutedInterface = newRef; } - } - else - { - nextInstance = gi; - } - } - else - { - nextInstance = gi; - } - } - - // Emit GetInterface() / GetDefaultInterface() impl for this interface BEFORE its - // members (mirrors C++ write_class_interface at code_writers.h:4257-4280). For - // overridable interfaces or non-exclusive direct interfaces, emit - // IWindowsRuntimeInterface.GetInterface(). For the default interface on an - // unsealed class with an exclusive default, emit "internal new GetDefaultInterface()". - // - // The IWindowsRuntimeInterface markers are NOT emitted in ref mode (gated by - // !w.Settings.ReferenceProjection here, mirrors C++ code_writers.h:4257 - // '&& !settings.reference_projection' in the corresponding condition). The - // 'internal new GetDefaultInterface()' helper IS emitted in both modes since - // it's referenced by overrides on derived classes. - if (IsInterfaceInInheritanceList(impl, includeExclusiveInterface: false) && !w.Settings.ReferenceProjection) - { - string giObjRefName = GetObjRefName(w, substitutedInterface); - w.Write("\nWindowsRuntimeObjectReferenceValue IWindowsRuntimeInterface<"); - WriteInterfaceTypeNameForCcw(w, substitutedInterface); - w.Write(">.GetInterface()\n{\nreturn "); - w.Write(giObjRefName); - w.Write(".AsValue();\n}\n"); - } - else if (Helpers.IsDefaultInterface(impl) && !classType.IsSealed) - { - // Mirrors C++ code_writers.h:4263-4280. The C++ source emits the - // 'internal new GetDefaultInterface()' helper whenever the interface is the - // default interface and the class is unsealed -- regardless of exclusive-to - // status. In ref-projection mode this is the only branch that emits the helper - // (the prior 'IWindowsRuntimeInterface.GetInterface' branch is gated off). - // In non-ref mode this branch is only reached when the prior branch's - // IsInterfaceInInheritanceList check fails (i.e., ExclusiveTo default interfaces), - // because non-exclusive default interfaces are routed to the prior branch. - string giObjRefName = GetObjRefName(w, substitutedInterface); - bool hasBaseType = false; - if (classType.BaseType is not null) - { - string? baseNs = classType.BaseType.Namespace?.Value; - string? baseName = classType.BaseType.Name?.Value; - hasBaseType = !(baseNs == "System" && baseName == "Object"); - } - w.Write("\ninternal "); - if (hasBaseType) { w.Write("new "); } - w.Write("WindowsRuntimeObjectReferenceValue GetDefaultInterface()\n{\nreturn "); - w.Write(giObjRefName); - w.Write(".AsValue();\n}\n"); - } - - // For mapped interfaces with custom members output (e.g. IClosable -> IDisposable, IMap`2 - // -> IDictionary), emit stubs for the C# interface's required members so the class - // satisfies its inheritance contract. The runtime's adapter actually services them. - string ifaceNs = ifaceType.Namespace?.Value ?? string.Empty; - string ifaceName = ifaceType.Name?.Value ?? string.Empty; - if (MappedTypes.Get(ifaceNs, ifaceName) is { HasCustomMembersOutput: true }) - { - if (IsMappedInterfaceRequiringStubs(ifaceNs, ifaceName)) - { - // For generic interfaces, use the substituted nextInstance to compute the - // objref name so type arguments are concrete (matches the field name emitted - // by WriteClassObjRefDefinitions). For non-generic, fall back to impl.Interface. - string objRefName = GetObjRefName(w, substitutedInterface); - WriteMappedInterfaceStubs(w, nextInstance, ifaceName, objRefName); - } - continue; - } - - WriteInterfaceMembers(w, classType, ifaceType, impl.Interface, isOverridable, isProtected, nextInstance, - writtenMethods, propertyState, writtenEvents); - - // Recurse into derived interfaces - WriteInterfaceMembersRecursive(w, classType, ifaceType, nextInstance, writtenMethods, propertyState, writtenEvents, writtenInterfaces); - } - } - - private static TypeDefinition? ResolveInterface(ITypeDefOrRef typeRef) - { - if (typeRef is TypeDefinition td) { return td; } - if (_cacheRef is null) { return null; } - // Try the runtime context resolver first (handles cross-module references via the resolver) - try - { - TypeDefinition? resolved = typeRef.Resolve(_cacheRef.RuntimeContext); - if (resolved is not null) { return resolved; } - } - catch - { - // Fall through to local lookup - } - // Fall back to local lookup by full name - if (typeRef is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - string fullName = string.IsNullOrEmpty(ns) ? name : ns + "." + name; - return _cacheRef.Find(fullName); - } - if (typeRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) - { - return ResolveInterface(gi.GenericType); - } - return null; - } - - private static void WriteInterfaceMembers(TypeWriter w, TypeDefinition classType, TypeDefinition ifaceType, - ITypeDefOrRef originalInterface, - bool isOverridable, bool isProtected, AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? currentInstance, - HashSet writtenMethods, IDictionary propertyState, HashSet writtenEvents) - { - bool sealed_ = classType.IsSealed; - // Determine accessibility and method modifier. - // Overridable interfaces are emitted with 'protected' visibility, plus 'virtual' on - // non-sealed classes. Sealed classes still get 'protected' (without virtual). - string access = (isOverridable || isProtected) ? "protected " : "public "; - string methodSpec = string.Empty; - if (isOverridable && !sealed_) - { - methodSpec = "virtual "; - } - - AsmResolver.DotNet.Signatures.GenericContext? genCtx = currentInstance is not null - ? new AsmResolver.DotNet.Signatures.GenericContext(currentInstance, null) - : null; - - // Generic interfaces require UnsafeAccessor-based dispatch (real ABI lives in the - // post-build interop assembly). - bool isGenericInterface = ifaceType.GenericParameters.Count > 0; - - // Fast ABI: when this interface is exclusive_to a fast-abi class (and we're emitting - // class members, classType is that fast-abi class), dispatch routes through the - // default interface's ABI Methods class and objref instead of through this interface's - // own ABI Methods class. The native vtable bundles all exclusive interfaces' methods - // into the default interface's vtable in a fixed order. Mirrors C++ - // code_writers.h:4250-4251 (semantics_for_abi_call assignment) which redirects both - // static_iface_target and the objref to the default interface for fast-abi cases. - TypeDefinition abiInterface = ifaceType; - ITypeDefOrRef abiInterfaceRef = originalInterface; - bool isFastAbiExclusive = IsFastAbiClass(classType, w.Settings) && TypeCategorization.IsExclusiveTo(ifaceType); - bool isDefaultInterface = false; - if (isFastAbiExclusive) - { - (TypeDefinition? defaultIface, _) = GetFastAbiInterfaces(classType); - if (defaultIface is not null) - { - abiInterface = defaultIface; - abiInterfaceRef = defaultIface; - isDefaultInterface = ReferenceEquals(defaultIface, ifaceType); - } - } - - // Mirrors C++ code_writers.h:4293 — the 'inline_event_source_field' arg is - // '!is_fast_abi_iface || is_default_interface'. For events on a fast-abi non-default - // exclusive interface (e.g. ISimple5.Event0 on the Simple class), the inline - // _eventSource_X field pattern is WRONG: the slot computed from the interface's own - // method index is invalid (the runtime exposes only the merged ISimple vtable, not - // a separate ISimple5 vtable). Instead, dispatch through the default interface's - // ABI Methods class helper (e.g. ISimpleMethods.Event0(this, _objRef_..ISimple)) - // which uses the correct merged-vtable slot and a ConditionalWeakTable for caching. - bool inlineEventSourceField = !isFastAbiExclusive || isDefaultInterface; - - // Compute the ABI Methods static class name (e.g. "global::ABI.Windows.Foundation.IDeferralMethods") - // — note this is the ungenerified Methods class for generic interfaces (matches truth output). - // The _objRef_ field name uses the full instantiated interface name so generic instantiations - // (e.g. IAsyncOperation) get a per-instantiation field. - string abiClass = w.WriteTemp("%", new System.Action(_ => - { - WriteTypedefName(w, abiInterface, TypedefNameType.StaticAbiClass, true); - })); - if (!abiClass.StartsWith("global::", System.StringComparison.Ordinal)) - { - abiClass = "global::" + abiClass; - } - string objRef = GetObjRefName(w, abiInterfaceRef); - - // For generic interfaces, also compute the encoded parent type name (used in UnsafeAccessor - // function names) and the WinRT.Interop accessor type string (passed to UnsafeAccessorType). - string genericParentEncoded = string.Empty; - string genericInteropType = string.Empty; - if (isGenericInterface && currentInstance is not null) - { - string projectedParent = w.WriteTemp("%", new System.Action(_ => - WriteTypeName(w, TypeSemanticsFactory.Get(currentInstance), TypedefNameType.Projected, true))); - genericParentEncoded = EscapeTypeNameForIdentifier(projectedParent, stripGlobal: true); - genericInteropType = EncodeInteropTypeName(currentInstance, TypedefNameType.StaticAbiClass) + ", WinRT.Interop"; - } - - // Compute the platform attribute string from the interface type's [ContractVersion] - // attribute. In ref mode, this is prepended to each member emission so the projected - // class members carry [SupportedOSPlatform("WindowsX.Y.Z.0")] mirroring the interface's - // contract version. Only emitted in ref mode (WritePlatformAttribute internally returns - // immediately if not ref). Mirrors C++ code_writers.h:4290 - // 'auto platform_attribute = write_platform_attribute_temp(w, interface_type);'. - string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, ifaceType))); - - // Methods - foreach (MethodDefinition method in ifaceType.Methods) - { - if (Helpers.IsSpecial(method)) { continue; } - string name = method.Name?.Value ?? string.Empty; - // Track by full signature (name + each param's element-type code) to avoid trivial overload duplicates. - // This prevents collapsing distinct overloads like Format(double) and Format(ulong). - MethodSig sig = new(method, genCtx); - string key = BuildMethodSignatureKey(name, sig); - if (!writtenMethods.Add(key)) { continue; } - - // Detect a 'string ToString()' that overrides Object.ToString(). C++ uses 'override' - // here (and even forces 'string' as the return type). See code_writers.h:1942-1959. - string methodSpecForThis = methodSpec; - if (name == "ToString" && sig.Params.Count == 0 - && sig.ReturnType is AsmResolver.DotNet.Signatures.CorLibTypeSignature crt - && crt.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String) - { - methodSpecForThis = "override "; - } - - // Detect 'bool Equals(object obj)' and 'int GetHashCode()' that override their - // System.Object counterparts. Mirrors C++ helpers.h:566 (is_object_equals_method) and - // helpers.h:625 (is_object_hashcode_method) + code_writers.h:1962-1974: matching - // signature and return type -> 'override'; matching name only -> 'new'. - if (name == "Equals" && sig.Params.Count == 1) - { - AsmResolver.DotNet.Signatures.TypeSignature p0 = sig.Params[0].Type; - bool paramIsObject = p0 is AsmResolver.DotNet.Signatures.CorLibTypeSignature po - && po.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object; - bool returnsBool = sig.ReturnType is AsmResolver.DotNet.Signatures.CorLibTypeSignature ro - && ro.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean; - if (paramIsObject) - { - methodSpecForThis = returnsBool ? "override " : (methodSpecForThis + "new "); - } - } - else if (name == "GetHashCode" && sig.Params.Count == 0) - { - bool returnsInt = sig.ReturnType is AsmResolver.DotNet.Signatures.CorLibTypeSignature ri - && ri.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4; - methodSpecForThis = returnsInt ? "override " : (methodSpecForThis + "new "); - } - - if (isGenericInterface && !string.IsNullOrEmpty(genericInteropType)) - { - // Emit UnsafeAccessor static extern + body that dispatches through it. - string accessorName = genericParentEncoded + "_" + name; - w.Write("\n[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); - w.Write(name); - w.Write("\")]\n"); - w.Write("static extern "); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(accessorName); - w.Write("([UnsafeAccessorType(\""); - w.Write(genericInteropType); - w.Write("\")] object _, WindowsRuntimeObjectReference thisReference"); - for (int i = 0; i < sig.Params.Count; i++) - { - w.Write(", "); - WriteProjectionParameter(w, sig.Params[i]); - } - w.Write(");\n"); - - // Mirrors C++ code_writers.h:4292 — prepend the per-interface platform attribute - // string to each public method emission. In ref mode this produces e.g. - // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write(access); - w.Write(methodSpecForThis); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(name); - w.Write("("); - WriteParameterList(w, sig); - if (w.Settings.ReferenceProjection) - { - // Mirrors C++ write_unsafe_accessor_static_method_call (code_writers.h:1653) - // which emits 'throw null' in reference projection mode. - w.Write(") => throw null;\n"); - } - else - { - w.Write(") => "); - w.Write(accessorName); - w.Write("(null, "); - w.Write(objRef); - for (int i = 0; i < sig.Params.Count; i++) - { - w.Write(", "); - WriteParameterNameWithModifier(w, sig.Params[i]); - } - w.Write(");\n"); - } - } - else - { - w.Write("\n"); - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write(access); - w.Write(methodSpecForThis); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(name); - w.Write("("); - WriteParameterList(w, sig); - if (w.Settings.ReferenceProjection) - { - // Mirrors C++ write_abi_static_method_call (code_writers.h:1637) - // which emits 'throw null' in reference projection mode. - w.Write(") => throw null;\n"); - } - else - { - w.Write(") => "); - w.Write(abiClass); - w.Write("."); - w.Write(name); - w.Write("("); - w.Write(objRef); - for (int i = 0; i < sig.Params.Count; i++) - { - w.Write(", "); - WriteParameterNameWithModifier(w, sig.Params[i]); - } - w.Write(");\n"); - } - } - - // For overridable interface methods, emit an explicit interface implementation - // that delegates to the protected (and virtual on non-sealed) method. Mirrors C++ - // overridable interface pattern: - // T InterfaceName.MethodName(args) => MethodName(args); - if (isOverridable) - { - // Mirror C++ which carries the platform attribute on the explicit interface - // impl as well (since it shares the same originating interface). - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - WriteProjectionReturnType(w, sig); - w.Write(" "); - WriteInterfaceTypeNameForCcw(w, originalInterface); - w.Write("."); - w.Write(name); - w.Write("("); - WriteParameterList(w, sig); - w.Write(") => "); - w.Write(name); - w.Write("("); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(", "); } - WriteParameterNameWithModifier(w, sig.Params[i]); - } - w.Write(");\n"); - } - } - - // Properties: collect into propertyState (merging accessors from multiple interfaces). - // Track per-accessor origin so that the getter/setter dispatch to the right ABI Methods - // class on the right _objRef_ field. - foreach (PropertyDefinition prop in ifaceType.Properties) - { - string name = prop.Name?.Value ?? string.Empty; - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - if (!propertyState.TryGetValue(name, out PropertyAccessorState? state)) - { - state = new PropertyAccessorState - { - PropTypeText = WritePropType(w, prop, genCtx), - Access = access, - MethodSpec = methodSpec, - IsOverridable = isOverridable, - OverridableInterface = isOverridable ? originalInterface : null, - }; - propertyState[name] = state; - } - if (getter is not null && !state.HasGetter) - { - state.HasGetter = true; - state.GetterAbiClass = abiClass; - state.GetterObjRef = objRef; - state.GetterIsGeneric = isGenericInterface; - state.GetterGenericInteropType = genericInteropType; - state.GetterGenericAccessorName = isGenericInterface ? (genericParentEncoded + "_" + name) : string.Empty; - state.GetterPropTypeText = WritePropType(w, prop, genCtx); - // Mirror C++ getter_platform tracking (code_writers.h:4306, 4323). - state.GetterPlatformAttribute = platformAttribute; - } - if (setter is not null && !state.HasSetter) - { - state.HasSetter = true; - state.SetterAbiClass = abiClass; - state.SetterObjRef = objRef; - state.SetterIsGeneric = isGenericInterface; - state.SetterGenericInteropType = genericInteropType; - state.SetterGenericAccessorName = isGenericInterface ? (genericParentEncoded + "_" + name) : string.Empty; - state.SetterPropTypeText = WritePropType(w, prop, genCtx); - // Mirror C++ setter_platform tracking (code_writers.h:4308, 4330). - state.SetterPlatformAttribute = platformAttribute; - } - } - - // Events: emit the event with Subscribe/Unsubscribe through a per-event _eventSource_ - // backing property field that lazily constructs an EventHandlerEventSource for the event - // handler type. Mirrors C++ write_class_events_using_static_abi_methods + write_event. - foreach (EventDefinition evt in ifaceType.Events) - { - string name = evt.Name?.Value ?? string.Empty; - if (!writtenEvents.Add(name)) { continue; } - - // Compute event handler type and event source type strings. - AsmResolver.DotNet.Signatures.TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); - if (currentInstance is not null) - { - evtSig = evtSig.InstantiateGenericTypes(new AsmResolver.DotNet.Signatures.GenericContext(currentInstance, null)); - } - bool isGenericEvent = evtSig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature; - - // Special case for ICommand.CanExecuteChanged: the WinRT event handler is - // EventHandler but C# expects non-generic EventHandler. Use the non-generic - // EventHandlerEventSource backing field. Mirrors C++ write_event hard-coded fix. - bool isICommandCanExecuteChanged = name == "CanExecuteChanged" - && (ifaceType.FullName == "Microsoft.UI.Xaml.Input.ICommand" - || ifaceType.FullName == "Windows.UI.Xaml.Input.ICommand"); - - string eventSourceType; - if (isICommandCanExecuteChanged) - { - eventSourceType = "global::WindowsRuntime.InteropServices.EventHandlerEventSource"; - isGenericEvent = false; - } - else - { - eventSourceType = w.WriteTemp("%", new System.Action(_ => - WriteTypeName(w, TypeSemanticsFactory.Get(evtSig), TypedefNameType.EventSource, false))); - } - string eventSourceTypeFull = eventSourceType; - if (!eventSourceTypeFull.StartsWith("global::", System.StringComparison.Ordinal)) - { - eventSourceTypeFull = "global::" + eventSourceTypeFull; - } - // The "interop" type name string for the EventSource UnsafeAccessor (only needed for generic events). - string eventSourceInteropType = isGenericEvent - ? EncodeInteropTypeName(evtSig, TypedefNameType.EventSource) + ", WinRT.Interop" - : string.Empty; - - // Compute vtable index = method index in the interface vtable + 6 (for IInspectable methods). - // The add method is the first method of the event in the interface. - int methodIndex = 0; - foreach (MethodDefinition m in ifaceType.Methods) - { - if (m == evt.AddMethod) { break; } - methodIndex++; - } - int vtableIndex = 6 + methodIndex; - - // Emit the _eventSource_ property field — skipped in ref mode (the event - // accessors below become 'add => throw null;' / 'remove => throw null;' which - // don't reference the field, mirrors C++ where the inline_event_source_field - // path emits 'throw null' at code_writers.h:2215, 2238). Also skipped when the - // event must dispatch through the ABI Methods class instead (see - // 'inlineEventSourceField' computation above for fast-abi non-default exclusive). - if (!w.Settings.ReferenceProjection && inlineEventSourceField) - { - w.Write("\nprivate "); - w.Write(eventSourceTypeFull); - w.Write(" _eventSource_"); - w.Write(name); - w.Write("\n{\n get\n {\n"); - if (isGenericEvent && !string.IsNullOrEmpty(eventSourceInteropType)) - { - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.Constructor)]\n"); - w.Write(" [return: UnsafeAccessorType(\""); - w.Write(eventSourceInteropType); - w.Write("\")]\n"); - w.Write(" static extern object ctor(WindowsRuntimeObjectReference nativeObjectReference, int index);\n\n"); - } - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" "); - w.Write(eventSourceTypeFull); - w.Write(" MakeEventSource()\n {\n"); - w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(\n"); - w.Write(" location1: ref field,\n"); - w.Write(" value: "); - if (isGenericEvent) - { - w.Write("Unsafe.As<"); - w.Write(eventSourceTypeFull); - w.Write(">(ctor("); - w.Write(objRef); - w.Write(", "); - w.Write(vtableIndex.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write("))"); - } - else - { - w.Write("new "); - w.Write(eventSourceTypeFull); - w.Write("("); - w.Write(objRef); - w.Write(", "); - w.Write(vtableIndex.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(")"); - } - w.Write(",\n"); - w.Write(" comparand: null);\n\n"); - w.Write(" return field;\n }\n\n"); - w.Write(" return field ?? MakeEventSource();\n }\n}\n"); - } - - // Emit the public/protected event with Subscribe/Unsubscribe. - w.Write("\n"); - // Mirrors C++ code_writers.h:4293 — prepend the per-interface platform attribute - // string to each event emission. In ref mode this produces e.g. - // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write(access); - w.Write(methodSpec); - w.Write("event "); - WriteEventType(w, evt, currentInstance); - w.Write(" "); - w.Write(name); - w.Write("\n{\n"); - if (w.Settings.ReferenceProjection) - { - w.Write(" add => throw null;\n"); - w.Write(" remove => throw null;\n"); - } - else if (inlineEventSourceField) - { - w.Write(" add => _eventSource_"); - w.Write(name); - w.Write(".Subscribe(value);\n"); - w.Write(" remove => _eventSource_"); - w.Write(name); - w.Write(".Unsubscribe(value);\n"); - } - else - { - // Fast-abi non-default exclusive: dispatch through the default interface's - // ABI Methods class helper. Mirrors C++ code_writers.h write_event when - // inline_event_source_field is false (the default helper-based path). - // Example: Simple.Event0 (on ISimple5) becomes - // add => global::ABI.test_component_fast.ISimpleMethods.Event0((WindowsRuntimeObject)this, _objRef_test_component_fast_ISimple).Subscribe(value); - w.Write(" add => "); - w.Write(abiClass); - w.Write("."); - w.Write(name); - w.Write("((WindowsRuntimeObject)this, "); - w.Write(objRef); - w.Write(").Subscribe(value);\n"); - w.Write(" remove => "); - w.Write(abiClass); - w.Write("."); - w.Write(name); - w.Write("((WindowsRuntimeObject)this, "); - w.Write(objRef); - w.Write(").Unsubscribe(value);\n"); - } - w.Write("}\n"); - } - } - - /// - /// Writes a parameter name prefixed with its modifier (in/out/ref) for use as a call argument. - /// - private static void WriteParameterNameWithModifier(TypeWriter w, ParamInfo p) - { - ParamCategory cat = ParamHelpers.GetParamCategory(p); - switch (cat) - { - case ParamCategory.Out: - w.Write("out "); - break; - case ParamCategory.Ref: - w.Write("in "); - break; - case ParamCategory.ReceiveArray: - w.Write("out "); - break; - } - WriteParameterName(w, p); - } - - /// - /// Writes the projected name for an interface reference (TypeDefinition, TypeReference, or - /// generic instance), applying mapped-type remapping. Used inside IWindowsRuntimeInterface<T>. - /// - private static void WriteInterfaceTypeNameForCcw(TypeWriter w, ITypeDefOrRef ifaceType) - { - // If the reference is to a type in the same module, resolve to TypeDefinition so - // WriteTypedefName can drop the 'global::.' prefix when the namespace matches. - // Mirrors the C++ tool's behavior of emitting the bare interface name when in scope. - if (ifaceType is not TypeDefinition && ifaceType is not TypeSpecification && _cacheRef is not null) - { - try - { - TypeDefinition? resolved = ifaceType.Resolve(_cacheRef.RuntimeContext); - if (resolved is not null) { ifaceType = resolved; } - } - catch { /* leave as TypeReference */ } - } - if (ifaceType is TypeDefinition td) - { - WriteTypedefName(w, td, TypedefNameType.CCW, false); - WriteTypeParams(w, td); - } - else if (ifaceType is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - w.Write("global::"); - w.Write(ns); - w.Write("."); - w.WriteCode(name); - } - else if (ifaceType is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - ITypeDefOrRef gt = gi.GenericType; - string ns = gt.Namespace?.Value ?? string.Empty; - string name = gt.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - w.Write("global::"); - w.Write(ns); - w.Write("."); - w.WriteCode(name); - w.Write("<"); - for (int i = 0; i < gi.TypeArguments.Count; i++) - { - if (i > 0) { w.Write(", "); } - WriteTypeName(w, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, true); - } - w.Write(">"); - } - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Component.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Component.cs deleted file mode 100644 index 94d4fa0c9c..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Component.cs +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Concurrent; -using System.Collections.Generic; -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Component-mode helpers, mirroring functions in code_writers.h. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ add_metadata_type_entry. - public static void AddMetadataTypeEntry(TypeWriter w, TypeDefinition type, ConcurrentDictionary map) - { - if (!w.Settings.Component) { return; } - TypeCategory cat = TypeCategorization.GetCategory(type); - if ((cat == TypeCategory.Class && TypeCategorization.IsStatic(type)) || - (cat == TypeCategory.Interface && TypeCategorization.IsExclusiveTo(type))) - { - return; - } - string typeName = w.WriteTemp("%", new System.Action(_ => - { - WriteTypedefName(w, type, TypedefNameType.Projected, true); - WriteTypeParams(w, type); - })); - string metadataTypeName = w.WriteTemp("%", new System.Action(_ => - { - WriteTypedefName(w, type, TypedefNameType.CCW, true); - WriteTypeParams(w, type); - })); - _ = map.TryAdd(typeName, metadataTypeName); - } - - /// Mirrors C++ write_factory_class (simplified). - public static void WriteFactoryClass(TypeWriter w, TypeDefinition type) - { - string typeName = type.Name?.Value ?? string.Empty; - string typeNs = type.Namespace?.Value ?? string.Empty; - // Mirror C++ 'write_type_name(type, Projected)' which for an authored type produces 'global::.'. - string projectedTypeName = string.IsNullOrEmpty(typeNs) - ? $"global::{Helpers.StripBackticks(typeName)}" - : $"global::{typeNs}.{Helpers.StripBackticks(typeName)}"; - string factoryTypeName = $"{Helpers.StripBackticks(typeName)}ServerActivationFactory"; - bool isActivatable = !TypeCategorization.IsStatic(type) && Helpers.HasDefaultConstructor(type); - - // Build the inheritance list: factory interfaces ([Activatable]/[Static]) only. - // Mirrors C++ write_factory_class_inheritance. - MetadataCache? cache = GetMetadataCache(); - List factoryInterfaces = new(); - if (cache is not null) - { - foreach (KeyValuePair kv in AttributedTypes.Get(type, cache)) - { - AttributedType info = kv.Value; - if ((info.Activatable || info.Statics) && info.Type is not null) - { - factoryInterfaces.Add(info.Type); - } - } - } - - w.Write("\ninternal sealed class "); - w.Write(factoryTypeName); - w.Write(" : global::WindowsRuntime.InteropServices.IActivationFactory"); - foreach (TypeDefinition iface in factoryInterfaces) - { - w.Write(", "); - // Mirror C++ 'write_type_name(factory.type, CCW, false)'. For factory interfaces, - // CCW + non-forced namespace is the user-facing interface name (e.g. 'IButtonUtilsStatic'). - WriteTypedefName(w, iface, TypedefNameType.CCW, false); - WriteTypeParams(w, iface); - } - w.Write("\n{\n"); - - w.Write("static "); - w.Write(factoryTypeName); - w.Write("()\n{\n"); - w.Write("global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof("); - w.Write(projectedTypeName); - w.Write(").TypeHandle);\n}\n"); - - w.Write("\npublic static unsafe void* Make()\n{\nreturn global::WindowsRuntime.InteropServices.Marshalling.WindowsRuntimeInterfaceMarshaller\n .ConvertToUnmanaged(_factory, in global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IActivationFactory)\n .DetachThisPtrUnsafe();\n}\n"); - - w.Write("\nprivate static readonly "); - w.Write(factoryTypeName); - w.Write(" _factory = new();\n"); - - w.Write("\npublic object ActivateInstance()\n{\n"); - if (isActivatable) - { - w.Write("return new "); - w.Write(projectedTypeName); - w.Write("();"); - } - else - { - w.Write("throw new NotImplementedException();"); - } - w.Write("\n}\n"); - - // Emit factory-class members: forwarding methods/properties/events for static factory - // interfaces, and constructor wrappers for activatable factory interfaces. - // Mirrors C++ write_factory_class_members. - if (cache is not null) - { - foreach (KeyValuePair kv in AttributedTypes.Get(type, cache)) - { - AttributedType info = kv.Value; - if (info.Type is null) { continue; } - - if (info.Activatable) - { - foreach (MethodDefinition method in info.Type.Methods) - { - if (method.IsConstructor) { continue; } - WriteFactoryActivatableMethod(w, method, projectedTypeName); - } - } - else if (info.Statics) - { - foreach (MethodDefinition method in info.Type.Methods) - { - if (method.IsConstructor) { continue; } - WriteStaticFactoryMethod(w, method, projectedTypeName); - } - foreach (PropertyDefinition prop in info.Type.Properties) - { - WriteStaticFactoryProperty(w, prop, projectedTypeName); - } - foreach (EventDefinition evt in info.Type.Events) - { - WriteStaticFactoryEvent(w, evt, projectedTypeName); - } - } - } - } - - w.Write("}\n"); - } - - /// - /// Writes a factory-class activatable wrapper method: public T MethodName(args) => new T(args);. - /// Mirrors C++ write_factory_activatable_method. - /// - private static void WriteFactoryActivatableMethod(TypeWriter w, MethodDefinition method, string projectedTypeName) - { - if (method.IsSpecialName) { return; } - string methodName = method.Name?.Value ?? string.Empty; - w.Write("\npublic "); - w.Write(projectedTypeName); - w.Write(" "); - w.Write(methodName); - w.Write("("); - WriteFactoryMethodParameters(w, method, includeTypes: true); - w.Write(") => new "); - w.Write(projectedTypeName); - w.Write("("); - WriteFactoryMethodParameters(w, method, includeTypes: false); - w.Write(");\n"); - } - - /// - /// Writes a static-factory forwarding method: public Ret MethodName(args) => global::Ns.Type.MethodName(args);. - /// Mirrors C++ write_static_factory_method. - /// - private static void WriteStaticFactoryMethod(TypeWriter w, MethodDefinition method, string projectedTypeName) - { - if (method.IsSpecialName) { return; } - string methodName = method.Name?.Value ?? string.Empty; - w.Write("\npublic "); - WriteFactoryReturnType(w, method); - w.Write(" "); - w.Write(methodName); - w.Write("("); - WriteFactoryMethodParameters(w, method, includeTypes: true); - w.Write(") => "); - w.Write(projectedTypeName); - w.Write("."); - w.Write(methodName); - w.Write("("); - WriteFactoryMethodParameters(w, method, includeTypes: false); - w.Write(");\n"); - } - - /// - /// Writes a static-factory forwarding property: a multi-line block matching C++ - /// write_property + write_static_factory_property. - /// - private static void WriteStaticFactoryProperty(TypeWriter w, PropertyDefinition prop, string projectedTypeName) - { - string propName = prop.Name?.Value ?? string.Empty; - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - // Single-line form when no setter is present (mirrors C++ early-return path). - if (setter is null) - { - w.Write("\npublic "); - WriteFactoryPropertyType(w, prop); - w.Write(" "); - w.Write(propName); - w.Write(" => "); - w.Write(projectedTypeName); - w.Write("."); - w.Write(propName); - w.Write(";\n"); - return; - } - w.Write("\npublic "); - WriteFactoryPropertyType(w, prop); - w.Write(" "); - w.Write(propName); - w.Write("\n{\n"); - if (getter is not null) - { - w.Write("get => "); - w.Write(projectedTypeName); - w.Write("."); - w.Write(propName); - w.Write(";\n"); - } - w.Write("set => "); - w.Write(projectedTypeName); - w.Write("."); - w.Write(propName); - w.Write(" = value;\n"); - w.Write("}\n"); - } - - /// - /// Writes a static-factory forwarding event as a multi-line block matching C++ - /// write_event + write_static_factory_event. - /// - private static void WriteStaticFactoryEvent(TypeWriter w, EventDefinition evt, string projectedTypeName) - { - string evtName = evt.Name?.Value ?? string.Empty; - w.Write("\npublic event "); - if (evt.EventType is not null) - { - TypeSemantics evtSemantics = TypeSemanticsFactory.GetFromTypeDefOrRef(evt.EventType); - WriteTypeName(w, evtSemantics, TypedefNameType.Projected, false); - } - w.Write(" "); - w.Write(evtName); - w.Write("\n{\n"); - w.Write("add => "); - w.Write(projectedTypeName); - w.Write("."); - w.Write(evtName); - w.Write(" += value;\n"); - w.Write("remove => "); - w.Write(projectedTypeName); - w.Write("."); - w.Write(evtName); - w.Write(" -= value;\n"); - w.Write("}\n"); - } - - private static void WriteFactoryReturnType(TypeWriter w, MethodDefinition method) - { - AsmResolver.DotNet.Signatures.TypeSignature? returnType = method.Signature?.ReturnType; - if (returnType is null || returnType.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Void) - { - w.Write("void"); - return; - } - TypeSemantics semantics = TypeSemanticsFactory.Get(returnType); - WriteTypeName(w, semantics, TypedefNameType.Projected, true); - } - - private static void WriteFactoryPropertyType(TypeWriter w, PropertyDefinition prop) - { - AsmResolver.DotNet.Signatures.TypeSignature? sig = prop.Signature?.ReturnType; - if (sig is null) { w.Write("object"); return; } - TypeSemantics semantics = TypeSemanticsFactory.Get(sig); - WriteTypeName(w, semantics, TypedefNameType.Projected, true); - } - - private static void WriteFactoryMethodParameters(TypeWriter w, MethodDefinition method, bool includeTypes) - { - AsmResolver.DotNet.Signatures.MethodSignature? sig = method.Signature; - if (sig is null) { return; } - for (int i = 0; i < sig.ParameterTypes.Count; i++) - { - if (i > 0) { w.Write(", "); } - ParameterDefinition? p = method.Parameters.Count > i + (method.IsStatic ? 0 : 0) ? method.Parameters[i].Definition : null; - string paramName = p?.Name?.Value ?? $"arg{i}"; - if (includeTypes) - { - TypeSemantics semantics = TypeSemanticsFactory.Get(sig.ParameterTypes[i]); - WriteTypeName(w, semantics, TypedefNameType.Projected, true); - w.Write(" "); - w.Write(paramName); - } - else - { - w.Write(paramName); - } - } - } - - /// Mirrors C++ write_module_activation_factory (simplified). - public static void WriteModuleActivationFactory(TextWriter w, IReadOnlyDictionary> typesByModule) - { - w.Write("\nusing System;\n"); - foreach (KeyValuePair> kv in typesByModule) - { - w.Write("\nnamespace ABI."); - w.Write(kv.Key); - w.Write("\n{\npublic static class ManagedExports\n{\npublic static unsafe void* GetActivationFactory(ReadOnlySpan activatableClassId)\n{\nswitch (activatableClassId)\n{\n"); - // Sort by the type's metadata token / row index so cases appear in WinMD declaration - // order. Mirrors C++ which uses std::set (sorted by metadata RID). - List orderedTypes = new(kv.Value); - orderedTypes.Sort((a, b) => - { - uint ra = a.MetadataToken.Rid; - uint rb = b.MetadataToken.Rid; - return ra.CompareTo(rb); - }); - foreach (TypeDefinition type in orderedTypes) - { - string ns = type.Namespace?.Value ?? string.Empty; - string name = type.Name?.Value ?? string.Empty; - w.Write("case \""); - w.Write(ns); - w.Write("."); - w.Write(name); - w.Write("\":\n return "); - // Mirror C++ 'write_type_name(type, CCW, true)' which for an authored type - // emits 'global::ABI.Impl..'. - w.Write("global::ABI.Impl."); - w.Write(ns); - w.Write("."); - w.Write(Helpers.StripBackticks(name)); - w.Write("ServerActivationFactory.Make();\n"); - } - w.Write("default:\n return null;\n}\n}\n}\n}\n"); - } - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Constructors.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Constructors.cs deleted file mode 100644 index 374de602dc..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Constructors.cs +++ /dev/null @@ -1,1026 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Activator/composer constructor emission. Mirrors C++ write_factory_constructors -/// and write_composable_constructors. -/// -internal static partial class CodeWriters -{ - /// - /// Mirrors C++ write_attributed_types: emits constructors and static members - /// for the given runtime class. - /// - public static void WriteAttributedTypes(TypeWriter w, TypeDefinition classType) - { - if (_cacheRef is null) { return; } - - // Track whether we need to emit the static _objRef_ field (used by - // default constructors). Emit it once per class if any [Activatable] factory exists. - bool needsClassObjRef = false; - - foreach (KeyValuePair kv in AttributedTypes.Get(classType, _cacheRef)) - { - AttributedType factory = kv.Value; - if (factory.Activatable && factory.Type is null) - { - needsClassObjRef = true; - break; - } - } - - if (needsClassObjRef) - { - string fullName = (classType.Namespace?.Value ?? string.Empty) + "." + (classType.Name?.Value ?? string.Empty); - string objRefName = "_objRef_" + EscapeTypeNameForIdentifier("global::" + fullName, stripGlobal: true); - w.Write("\nprivate static WindowsRuntimeObjectReference "); - w.Write(objRefName); - if (w.Settings.ReferenceProjection) - { - // Mirrors C++ write_activation_factory_objref_definition (code_writers.h:2748): - // in ref mode the activation factory objref getter body is just 'throw null;'. - EmitRefModeObjRefGetterBody(w); - } - else - { - w.Write("\n{\n get\n {\n var __"); - w.Write(objRefName); - w.Write(" = field;\n if (__"); - w.Write(objRefName); - w.Write(" != null && __"); - w.Write(objRefName); - w.Write(".IsInCurrentContext)\n {\n return __"); - w.Write(objRefName); - w.Write(";\n }\n return field = WindowsRuntimeObjectReference.GetActivationFactory(\""); - w.Write(fullName); - w.Write("\");\n }\n}\n"); - } - } - - foreach (KeyValuePair kv in AttributedTypes.Get(classType, _cacheRef)) - { - AttributedType factory = kv.Value; - if (factory.Activatable) - { - WriteFactoryConstructors(w, factory.Type, classType); - } - else if (factory.Composable) - { - WriteComposableConstructors(w, factory.Type, classType, factory.Visible ? "public" : "protected"); - } - } - } - - /// - /// Mirrors C++ write_factory_constructors. - /// - public static void WriteFactoryConstructors(TypeWriter w, TypeDefinition? factoryType, TypeDefinition classType) - { - string typeName = classType.Name?.Value ?? string.Empty; - int gcPressure = GetGcPressureAmount(classType); - if (factoryType is not null) - { - // Emit the factory objref property (lazy-initialized). - string factoryRuntimeClassFullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; - string factoryObjRefName = GetObjRefName(w, factoryType); - WriteStaticFactoryObjRef(w, factoryType, factoryRuntimeClassFullName, factoryObjRefName); - - string defaultIfaceIid = GetDefaultInterfaceIid(w, classType); - string marshalingType = GetMarshalingTypeName(classType); - // Compute the platform attribute string from the activation factory interface's - // [ContractVersion] attribute. Mirrors C++ code_writers.h:2861 - // 'auto platform_attribute = write_platform_attribute_temp(w, factory_type);' - // emitted at line 2872 before the public ctor. - string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, factoryType))); - int methodIndex = 0; - foreach (MethodDefinition method in factoryType.Methods) - { - if (Helpers.IsSpecial(method)) { methodIndex++; continue; } - MethodSig sig = new(method); - string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Params.Count.ToString(System.Globalization.CultureInfo.InvariantCulture); - string argsName = callbackName + "Args"; - - // Emit the public constructor. - w.Write("\n"); - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write("public unsafe "); - w.Write(typeName); - w.Write("("); - WriteParameterList(w, sig); - w.Write(")\n :base("); - if (sig.Params.Count == 0) - { - w.Write("default"); - } - else - { - w.Write(callbackName); - w.Write(".Instance, "); - w.Write(defaultIfaceIid); - w.Write(", "); - w.Write(marshalingType); - w.Write(", WindowsRuntimeActivationArgsReference.CreateUnsafe(new "); - w.Write(argsName); - w.Write("("); - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(", "); } - string raw = sig.Params[i].Parameter.Name ?? "param"; - w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); - } - w.Write("))"); - } - w.Write(")\n{\n"); - if (gcPressure > 0) - { - w.Write("GC.AddMemoryPressure("); - w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(");\n"); - } - w.Write("}\n"); - - if (sig.Params.Count > 0) - { - EmitFactoryArgsStruct(w, sig, argsName); - EmitFactoryCallbackClass(w, sig, callbackName, argsName, factoryObjRefName, methodIndex); - } - - methodIndex++; - } - } - else - { - // No factory type means [Activatable(uint version)] - emit a default ctor that calls - // the WindowsRuntimeObject base constructor with the activation factory objref. - // The default interface IID is needed too. - string fullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; - string objRefName = "_objRef_" + EscapeTypeNameForIdentifier("global::" + fullName, stripGlobal: true); - - // Find the default interface IID to use. - string defaultIfaceIid = GetDefaultInterfaceIid(w, classType); - - w.Write("\npublic "); - w.Write(typeName); - w.Write("()\n :base(default(WindowsRuntimeActivationTypes.DerivedSealed), "); - w.Write(objRefName); - w.Write(", "); - w.Write(defaultIfaceIid); - w.Write(", "); - w.Write(GetMarshalingTypeName(classType)); - w.Write(")\n{\n"); - if (gcPressure > 0) - { - w.Write("GC.AddMemoryPressure("); - w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(");\n"); - } - w.Write("}\n"); - } - } - - /// - /// Reads the [MarshalingBehaviorAttribute] on the class and returns the corresponding - /// CreateObjectReferenceMarshalingType.* expression. Mirrors C++ - /// get_marshaling_type_name. - /// - private static string GetMarshalingTypeName(TypeDefinition classType) - { - for (int i = 0; i < classType.CustomAttributes.Count; i++) - { - CustomAttribute attr = classType.CustomAttributes[i]; - ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; - if (attrType is null) { continue; } - if (attrType.Namespace?.Value != "Windows.Foundation.Metadata" || - attrType.Name?.Value != "MarshalingBehaviorAttribute") { continue; } - if (attr.Signature is null) { continue; } - for (int j = 0; j < attr.Signature.FixedArguments.Count; j++) - { - AsmResolver.DotNet.Signatures.CustomAttributeArgument arg = attr.Signature.FixedArguments[j]; - if (arg.Element is int v) - { - return v switch - { - 2 => "CreateObjectReferenceMarshalingType.Agile", - 3 => "CreateObjectReferenceMarshalingType.Standard", - _ => "CreateObjectReferenceMarshalingType.Unknown", - }; - } - } - } - return "CreateObjectReferenceMarshalingType.Unknown"; - } - - /// Emits the private readonly ref struct <Name>Args(args...) {...}. - /// If >= 0, only emit the first - /// params (used for composable factories where the trailing baseInterface/innerInterface params - /// are consumed by the callback Invoke signature directly, not stored in args). - private static void EmitFactoryArgsStruct(TypeWriter w, MethodSig sig, string argsName, int userParamCount = -1) - { - int count = userParamCount >= 0 ? userParamCount : sig.Params.Count; - w.Write("\nprivate readonly ref struct "); - w.Write(argsName); - w.Write("("); - for (int i = 0; i < count; i++) - { - if (i > 0) { w.Write(", "); } - WriteProjectionParameter(w, sig.Params[i]); - } - w.Write(")\n{\n"); - for (int i = 0; i < count; i++) - { - ParamInfo p = sig.Params[i]; - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(" public readonly "); - // Use the parameter's projected type (matches the constructor parameter type, including - // ReadOnlySpan/Span for array params). - WriteProjectionParameterType(w, p); - w.Write(" "); - w.Write(pname); - w.Write(" = "); - w.Write(pname); - w.Write(";\n"); - } - w.Write("}\n"); - } - - /// Emits the private sealed class <Name> : WindowsRuntimeActivationFactoryCallback.DerivedSealed. - /// When true, emit the DerivedComposed callback variant whose - /// Invoke signature includes the additional WindowsRuntimeObject baseInterface + - /// out void* innerInterface params. Iteration over user params is bounded by - /// (defaults to all params). - private static void EmitFactoryCallbackClass(TypeWriter w, MethodSig sig, string callbackName, string argsName, string factoryObjRefName, int factoryMethodIndex, bool isComposable = false, int userParamCount = -1) - { - int paramCount = userParamCount >= 0 ? userParamCount : sig.Params.Count; - w.Write("\nprivate sealed class "); - w.Write(callbackName); - w.Write(isComposable - ? " : WindowsRuntimeActivationFactoryCallback.DerivedComposed\n{\n" - : " : WindowsRuntimeActivationFactoryCallback.DerivedSealed\n{\n"); - w.Write(" public static readonly "); - w.Write(callbackName); - w.Write(" Instance = new();\n\n"); - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - if (isComposable) - { - // Composable Invoke signature is multi-line and includes baseInterface (in) + - // innerInterface (out). Mirrors truth output exactly. - w.Write(" public override unsafe void Invoke(\n"); - w.Write(" WindowsRuntimeActivationArgsReference additionalParameters,\n"); - w.Write(" WindowsRuntimeObject baseInterface,\n"); - w.Write(" out void* innerInterface,\n"); - w.Write(" out void* retval)\n {\n"); - } - else - { - // Sealed Invoke signature is multi-line. Mirrors C++ at code_writers.h:6838. - w.Write(" public override unsafe void Invoke(\n"); - w.Write(" WindowsRuntimeActivationArgsReference additionalParameters,\n"); - w.Write(" out void* retval)\n {\n"); - } - - // Mirrors C++ at code_writers.h:6849: in reference projection mode, the entire - // Invoke body is just 'throw null;' (no factory dispatch, no marshalling). - if (w.Settings.ReferenceProjection) - { - EmitRefModeInvokeBody(w); - return; - } - - w.Write(" using WindowsRuntimeObjectReferenceValue activationFactoryValue = "); - w.Write(factoryObjRefName); - w.Write(".AsValue();\n"); - w.Write(" void* ThisPtr = activationFactoryValue.GetThisPtrUnsafe();\n"); - w.Write(" ref readonly "); - w.Write(argsName); - w.Write(" args = ref additionalParameters.GetValueRefUnsafe<"); - w.Write(argsName); - w.Write(">();\n"); - - // Bind each arg from the args struct to a local of its ABI-marshalable input type. - // Bind arg locals. - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - w.Write(" "); - // For array params, the bind type is ReadOnlySpan / Span (not the SzArray). - if (cat == ParamCategory.PassArray) - { - w.Write("ReadOnlySpan<"); - WriteProjectionType(w, TypeSemanticsFactory.Get(((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType)); - w.Write(">"); - } - else if (cat == ParamCategory.FillArray) - { - w.Write("Span<"); - WriteProjectionType(w, TypeSemanticsFactory.Get(((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType)); - w.Write(">"); - } - else - { - WriteProjectedSignature(w, p.Type, true); - } - w.Write(" "); - w.Write(pname); - w.Write(" = args."); - w.Write(pname); - w.Write(";\n"); - } - - // For generic instance params, emit local UnsafeAccessor delegates (or Nullable -> BoxToUnmanaged). - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - if (!IsGenericInstance(p.Type)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - if (IsNullableT(p.Type)) - { - AsmResolver.DotNet.Signatures.TypeSignature inner = GetNullableInnerType(p.Type)!; - string innerMarshaller = GetNullableInnerMarshallerName(w, inner); - w.Write(" using WindowsRuntimeObjectReferenceValue __"); - w.Write(raw); - w.Write(" = "); - w.Write(innerMarshaller); - w.Write(".BoxToUnmanaged("); - w.Write(pname); - w.Write(");\n"); - continue; - } - string interopTypeName = EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; - string projectedTypeName = w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, p.Type, false))); - w.Write(" [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"ConvertToUnmanaged\")]\n"); - w.Write(" static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopTypeName); - w.Write("\")] object _, "); - w.Write(projectedTypeName); - w.Write(" value);\n"); - w.Write(" using WindowsRuntimeObjectReferenceValue __"); - w.Write(raw); - w.Write(" = ConvertToUnmanaged_"); - w.Write(raw); - w.Write("(null, "); - w.Write(pname); - w.Write(");\n"); - } - - // For runtime class / object params, emit `using WindowsRuntimeObjectReferenceValue __ = ...ConvertToUnmanaged();` - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - if (IsGenericInstance(p.Type)) { continue; } // already handled above - if (!IsRuntimeClassOrInterface(p.Type) && !IsObject(p.Type)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(" using WindowsRuntimeObjectReferenceValue __"); - w.Write(raw); - w.Write(" = "); - EmitMarshallerConvertToUnmanaged(w, p.Type, pname); - w.Write(";\n"); - } - - // For composable factories, marshal the additional `baseInterface` (which is a - // WindowsRuntimeObject parameter on Invoke, not an args field). Truth pattern: - // using WindowsRuntimeObjectReferenceValue __baseInterface = WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(baseInterface); - if (isComposable) - { - w.Write(" using WindowsRuntimeObjectReferenceValue __baseInterface = WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(baseInterface);\n"); - w.Write(" void* __innerInterface = default;\n"); - } - - // For mapped value-type params (DateTime, TimeSpan), emit ABI local + marshaller conversion. - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - if (!IsMappedAbiValueType(p.Type)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - string abiType = GetMappedAbiTypeName(p.Type); - string marshaller = GetMappedMarshallerName(p.Type); - w.Write(" "); - w.Write(abiType); - w.Write(" __"); - w.Write(raw); - w.Write(" = "); - w.Write(marshaller); - w.Write(".ConvertToUnmanaged("); - w.Write(pname); - w.Write(");\n"); - } - - // For HResultException params, emit ABI local + ExceptionMarshaller conversion. - // (HResult is excluded from IsMappedAbiValueType because it's "treated specially in many - // places", but for activator factory ctor params the marshalling pattern is the same.) - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - if (!IsHResultException(p.Type)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(" global::ABI.System.Exception __"); - w.Write(raw); - w.Write(" = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged("); - w.Write(pname); - w.Write(");\n"); - } - - // Declare InlineArray16 + ArrayPool fallback for non-blittable PassArray params - // (runtime classes, objects, strings). - bool hasNonBlittableArray = false; - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - hasNonBlittableArray = true; - string raw = p.Parameter.Name ?? "param"; - string callName = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); - w.Write(raw); - w.Write("_inlineArray);\n"); - w.Write(" nint[] __"); - w.Write(raw); - w.Write("_arrayFromPool = null;\n"); - w.Write(" Span __"); - w.Write(raw); - w.Write("_span = "); - w.Write(callName); - w.Write(".Length <= 16\n ? __"); - w.Write(raw); - w.Write("_inlineArray[.."); - w.Write(callName); - w.Write(".Length]\n : (__"); - w.Write(raw); - w.Write("_arrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); - w.Write(callName); - w.Write(".Length));\n"); - - if (IsString(szArr.BaseType)) - { - w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); - w.Write(raw); - w.Write("_inlineHeaderArray);\n"); - w.Write(" HStringHeader[] __"); - w.Write(raw); - w.Write("_headerArrayFromPool = null;\n"); - w.Write(" Span __"); - w.Write(raw); - w.Write("_headerSpan = "); - w.Write(callName); - w.Write(".Length <= 16\n ? __"); - w.Write(raw); - w.Write("_inlineHeaderArray[.."); - w.Write(callName); - w.Write(".Length]\n : (__"); - w.Write(raw); - w.Write("_headerArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); - w.Write(callName); - w.Write(".Length));\n"); - - w.Write("\n Unsafe.SkipInit(out InlineArray16 __"); - w.Write(raw); - w.Write("_inlinePinnedHandleArray);\n"); - w.Write(" nint[] __"); - w.Write(raw); - w.Write("_pinnedHandleArrayFromPool = null;\n"); - w.Write(" Span __"); - w.Write(raw); - w.Write("_pinnedHandleSpan = "); - w.Write(callName); - w.Write(".Length <= 16\n ? __"); - w.Write(raw); - w.Write("_inlinePinnedHandleArray[.."); - w.Write(callName); - w.Write(".Length]\n : (__"); - w.Write(raw); - w.Write("_pinnedHandleArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent("); - w.Write(callName); - w.Write(".Length));\n"); - } - } - - w.Write(" void* __retval = default;\n"); - if (hasNonBlittableArray) { w.Write(" try\n {\n"); } - string baseIndent = hasNonBlittableArray ? " " : " "; - - // For System.Type params, pre-marshal to TypeReference (must be declared OUTSIDE the - // fixed() block since the fixed block pins the resulting reference). - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - if (!IsSystemType(p.Type)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(baseIndent); - w.Write("global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe("); - w.Write(pname); - w.Write(", out TypeReference __"); - w.Write(raw); - w.Write(");\n"); - } - - // Open ONE combined "fixed(void* _a = ..., _b = ..., ...)" block for ALL pinnable - // params (string, Type, PassArray). Mirrors C++ write_abi_method_call_marshalers - // which emits a single combined fixed-block for all is_pinnable marshalers. - int fixedNesting = 0; - int pinnableCount = 0; - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (IsString(p.Type) || IsSystemType(p.Type)) { pinnableCount++; } - else if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) { pinnableCount++; } - } - if (pinnableCount > 0) - { - string indent = baseIndent; - w.Write(indent); - w.Write("fixed(void* "); - bool firstPin = true; - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - bool isStr = IsString(p.Type); - bool isType = IsSystemType(p.Type); - bool isArr = cat == ParamCategory.PassArray || cat == ParamCategory.FillArray; - if (!isStr && !isType && !isArr) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - if (!firstPin) { w.Write(", "); } - firstPin = false; - w.Write("_"); - w.Write(raw); - w.Write(" = "); - if (isType) { w.Write("__"); w.Write(raw); } - else if (isArr) - { - AsmResolver.DotNet.Signatures.TypeSignature elemT = ((AsmResolver.DotNet.Signatures.SzArrayTypeSignature)p.Type).BaseType; - bool isBlittableElem = IsBlittablePrimitive(elemT) || IsAnyStruct(elemT); - bool isStringElem = IsString(elemT); - if (isBlittableElem) { w.Write(pname); } - else { w.Write("__"); w.Write(raw); w.Write("_span"); } - if (isStringElem) - { - w.Write(", _"); - w.Write(raw); - w.Write("_inlineHeaderArray = __"); - w.Write(raw); - w.Write("_headerSpan"); - } - } - else - { - // string param: pin the input string itself. - w.Write(pname); - } - } - w.Write(")\n"); - w.Write(indent); - w.Write("{\n"); - fixedNesting = 1; - // Inside the block: emit HStringMarshaller.ConvertToUnmanagedUnsafe for each - // string input. The HStringReference local lives stack-only. - string innerIndent = baseIndent + new string(' ', fixedNesting * 4); - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - if (!IsString(p.Type)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(innerIndent); - w.Write("HStringMarshaller.ConvertToUnmanagedUnsafe((char*)_"); - w.Write(raw); - w.Write(", "); - w.Write(pname); - w.Write("?.Length, out HStringReference __"); - w.Write(raw); - w.Write(");\n"); - } - } - - string callIndent = baseIndent + new string(' ', fixedNesting * 4); - - // Emit CopyToUnmanaged for non-blittable PassArray params. - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - if (IsString(szArr.BaseType)) - { - w.Write(callIndent); - w.Write("HStringArrayMarshaller.ConvertToUnmanagedUnsafe(\n"); - w.Write(callIndent); - w.Write(" source: "); - w.Write(pname); - w.Write(",\n"); - w.Write(callIndent); - w.Write(" hstringHeaders: (HStringHeader*) _"); - w.Write(raw); - w.Write("_inlineHeaderArray,\n"); - w.Write(callIndent); - w.Write(" hstrings: __"); - w.Write(raw); - w.Write("_span,\n"); - w.Write(callIndent); - w.Write(" pinnedGCHandles: __"); - w.Write(raw); - w.Write("_pinnedHandleSpan);\n"); - } - else - { - string elementProjected = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, TypeSemanticsFactory.Get(szArr.BaseType)))); - string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); - w.Write(callIndent); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"CopyToUnmanaged\")]\n"); - w.Write(callIndent); - w.Write("static extern void CopyToUnmanaged_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); - w.Write("\")] object _, ReadOnlySpan<"); - w.Write(elementProjected); - w.Write("> span, uint length, void** data);\n"); - w.Write(callIndent); - w.Write("CopyToUnmanaged_"); - w.Write(raw); - w.Write("(null, "); - w.Write(pname); - w.Write(", (uint)"); - w.Write(pname); - w.Write(".Length, (void**)_"); - w.Write(raw); - w.Write(");\n"); - } - } - - w.Write(callIndent); - // delegate* signature: void*, then each ABI param type, then [void*, void**] (composable), - // then void**, then int. - w.Write("RestrictedErrorInfo.ThrowExceptionForHR((*(delegate* unmanaged[MemberFunction]**)ThisPtr)["); - w.Write((6 + factoryMethodIndex).ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write("](ThisPtr"); - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - string raw = p.Parameter.Name ?? "param"; - string pname = Helpers.IsKeyword(raw) ? "@" + raw : raw; - w.Write(",\n "); - if (cat == ParamCategory.PassArray || cat == ParamCategory.FillArray) - { - w.Write("(uint)"); - w.Write(pname); - w.Write(".Length, _"); - w.Write(raw); - continue; - } - // For enums, cast to underlying type. For bool, cast to byte. For char, cast to ushort. - // For string params, use the marshalled HString from the fixed block. - // For runtime class / object / generic instance params, use __.GetThisPtrUnsafe(). - if (IsEnumType(p.Type)) - { - // No cast needed: function pointer signature uses the projected enum type. - w.Write(pname); - } - else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibBool && - corlibBool.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean) - { - w.Write(pname); - } - else if (p.Type is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlibChar && - corlibChar.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char) - { - w.Write(pname); - } - else if (IsString(p.Type)) - { - w.Write("__"); - w.Write(raw); - w.Write(".HString"); - } - else if (IsSystemType(p.Type)) - { - w.Write("__"); - w.Write(raw); - w.Write(".ConvertToUnmanagedUnsafe()"); - } - else if (IsRuntimeClassOrInterface(p.Type) || IsObject(p.Type) || IsGenericInstance(p.Type)) - { - w.Write("__"); - w.Write(raw); - w.Write(".GetThisPtrUnsafe()"); - } - else if (IsMappedAbiValueType(p.Type)) - { - w.Write("__"); - w.Write(raw); - } - else if (IsHResultException(p.Type)) - { - w.Write("__"); - w.Write(raw); - } - else - { - w.Write(pname); - } - } - if (isComposable) - { - // Pass __baseInterface.GetThisPtrUnsafe() and &__innerInterface. - w.Write(",\n __baseInterface.GetThisPtrUnsafe(),\n &__innerInterface"); - } - w.Write(",\n &__retval));\n"); - if (isComposable) - { - w.Write(callIndent); - w.Write("innerInterface = __innerInterface;\n"); - } - w.Write(callIndent); - w.Write("retval = __retval;\n"); - - // Close fixed blocks (innermost first). - for (int i = fixedNesting - 1; i >= 0; i--) - { - string indent = baseIndent + new string(' ', i * 4); - w.Write(indent); - w.Write("}\n"); - } - - // Close try and emit finally with cleanup for non-blittable PassArray params. - if (hasNonBlittableArray) - { - w.Write(" }\n finally\n {\n"); - for (int i = 0; i < paramCount; i++) - { - ParamInfo p = sig.Params[i]; - ParamCategory cat = ParamHelpers.GetParamCategory(p); - if (cat != ParamCategory.PassArray && cat != ParamCategory.FillArray) { continue; } - if (p.Type is not AsmResolver.DotNet.Signatures.SzArrayTypeSignature szArr) { continue; } - if (IsBlittablePrimitive(szArr.BaseType) || IsAnyStruct(szArr.BaseType)) { continue; } - string raw = p.Parameter.Name ?? "param"; - if (IsString(szArr.BaseType)) - { - w.Write("\n HStringArrayMarshaller.Dispose(__"); - w.Write(raw); - w.Write("_pinnedHandleSpan);\n\n"); - w.Write(" if (__"); - w.Write(raw); - w.Write("_pinnedHandleArrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(raw); - w.Write("_pinnedHandleArrayFromPool);\n }\n\n"); - w.Write(" if (__"); - w.Write(raw); - w.Write("_headerArrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(raw); - w.Write("_headerArrayFromPool);\n }\n"); - } - else - { - string elementInteropArg = EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); - w.Write("\n [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"Dispose\")]\n"); - w.Write(" static extern void Dispose_"); - w.Write(raw); - w.Write("([UnsafeAccessorType(\""); - w.Write(GetArrayMarshallerInteropPath(w, szArr.BaseType, elementInteropArg)); - w.Write("\")] object _, uint length, void** data);\n\n"); - w.Write(" fixed(void* _"); - w.Write(raw); - w.Write(" = __"); - w.Write(raw); - w.Write("_span)\n {\n"); - w.Write(" Dispose_"); - w.Write(raw); - w.Write("(null, (uint) __"); - w.Write(raw); - w.Write("_span.Length, (void**)_"); - w.Write(raw); - w.Write(");\n }\n"); - } - w.Write("\n if (__"); - w.Write(raw); - w.Write("_arrayFromPool is not null)\n {\n"); - w.Write(" global::System.Buffers.ArrayPool.Shared.Return(__"); - w.Write(raw); - w.Write("_arrayFromPool);\n }\n"); - } - w.Write(" }\n"); - } - - w.Write(" }\n}\n"); - } - - /// Returns the IID expression for the class's default interface. - private static string GetDefaultInterfaceIid(TypeWriter w, TypeDefinition classType) - { - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(classType); - if (defaultIface is null) { return "default(global::System.Guid)"; } - return w.WriteTemp("%", new System.Action(_ => WriteIidExpression(w, defaultIface))); - } - - /// - /// Mirrors C++ write_composable_constructors. - /// Emits: - /// 1. Public/protected constructors for each composable factory method (with proper body). - /// 2. Static factory callback class (per ctor) for parameterized composable activation. - /// 3. Four protected base-chaining constructors used by derived projected types. - /// - public static void WriteComposableConstructors(TypeWriter w, TypeDefinition? composableType, TypeDefinition classType, string visibility) - { - if (composableType is null) { return; } - string typeName = classType.Name?.Value ?? string.Empty; - - // Emit the factory objref + IIDs at the top so the parameterized ctors can reference it. - if (composableType.Methods.Count > 0) - { - string runtimeClassFullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; - string factoryObjRefName = GetObjRefName(w, composableType); - WriteStaticFactoryObjRef(w, composableType, runtimeClassFullName, factoryObjRefName); - } - - string defaultIfaceIid = GetDefaultInterfaceIid(w, classType); - string marshalingType = GetMarshalingTypeName(classType); - string defaultIfaceObjRef; - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(classType); - defaultIfaceObjRef = defaultIface is not null ? GetObjRefName(w, defaultIface) : string.Empty; - int gcPressure = GetGcPressureAmount(classType); - // Compute the platform attribute string from the composable factory interface's - // [ContractVersion] attribute. Mirrors C++ code_writers.h:3167 - // 'auto platform_attribute = write_platform_attribute_temp(w, composable_type);' - // emitted at line 3179 before the public ctor. - string platformAttribute = w.WriteTemp("%", new System.Action(_ => WritePlatformAttribute(w, composableType))); - - int methodIndex = 0; - foreach (MethodDefinition method in composableType.Methods) - { - if (Helpers.IsSpecial(method)) { methodIndex++; continue; } - // Composable factory methods have signature like: - // T CreateInstance(args, object baseInterface, out object innerInterface) - // For the constructor on the projected class, we exclude the trailing two params. - MethodSig sig = new(method); - int userParamCount = sig.Params.Count >= 2 ? sig.Params.Count - 2 : sig.Params.Count; - // Mirror C++ write_constructor_callback_method_name (code_writers.h:2635-2643): - // the callback / args type name suffix is the TOTAL ABI param count - // (size(method.Signature().Params())), NOT the user-visible param count. Using the - // total count guarantees uniqueness against other composable factory overloads that - // might share the same user-param count but differ in trailing baseInterface shape. - string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Params.Count.ToString(System.Globalization.CultureInfo.InvariantCulture); - string argsName = callbackName + "Args"; - bool isParameterless = userParamCount == 0; - - w.Write("\n"); - if (!string.IsNullOrEmpty(platformAttribute)) { w.Write(platformAttribute); } - w.Write(visibility); - if (!isParameterless) { w.Write(" unsafe "); } else { w.Write(" "); } - w.Write(typeName); - w.Write("("); - for (int i = 0; i < userParamCount; i++) - { - if (i > 0) { w.Write(", "); } - WriteProjectionParameter(w, sig.Params[i]); - } - w.Write(")\n :base("); - if (isParameterless) - { - // base(default(WindowsRuntimeActivationTypes.DerivedComposed), , , ) - string factoryObjRef = GetObjRefName(w, composableType); - w.Write("default(WindowsRuntimeActivationTypes.DerivedComposed), "); - w.Write(factoryObjRef); - w.Write(", "); - w.Write(defaultIfaceIid); - w.Write(", "); - w.Write(marshalingType); - } - else - { - w.Write(callbackName); - w.Write(".Instance, "); - w.Write(defaultIfaceIid); - w.Write(", "); - w.Write(marshalingType); - w.Write(", WindowsRuntimeActivationArgsReference.CreateUnsafe(new "); - w.Write(argsName); - w.Write("("); - for (int i = 0; i < userParamCount; i++) - { - if (i > 0) { w.Write(", "); } - string raw = sig.Params[i].Parameter.Name ?? "param"; - w.Write(Helpers.IsKeyword(raw) ? "@" + raw : raw); - } - w.Write("))"); - } - w.Write(")\n{\n"); - w.Write("if (GetType() == typeof("); - w.Write(typeName); - w.Write("))\n{\n"); - if (!string.IsNullOrEmpty(defaultIfaceObjRef)) - { - w.Write(defaultIfaceObjRef); - w.Write(" = NativeObjectReference;\n"); - } - w.Write("}\n"); - if (gcPressure > 0) - { - w.Write("GC.AddMemoryPressure("); - w.Write(gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture)); - w.Write(");\n"); - } - w.Write("}\n"); - - // Emit args struct + callback class for parameterized composable factories. - // Mirrors C++ write_static_composing_factory_method (code_writers.h:6886) which - // skips both the args struct AND the callback class entirely in ref mode. The - // public ctor above still references these types, but reference assemblies don't - // need their bodies' references to resolve (only the public API surface matters). - if (!isParameterless && !w.Settings.ReferenceProjection) - { - EmitFactoryArgsStruct(w, sig, argsName, userParamCount); - string factoryObjRefName = GetObjRefName(w, composableType); - EmitFactoryCallbackClass(w, sig, callbackName, argsName, factoryObjRefName, methodIndex, isComposable: true, userParamCount: userParamCount); - } - - methodIndex++; - } - - if (w.Settings.ReferenceProjection) { return; } - - // Emit the four base-chaining constructors used by derived projected types. - string gcPressureBody = gcPressure > 0 - ? "GC.AddMemoryPressure(" + gcPressure.ToString(System.Globalization.CultureInfo.InvariantCulture) + ");\n" - : string.Empty; - - // 1. WindowsRuntimeActivationTypes.DerivedComposed - w.Write("\nprotected "); - w.Write(typeName); - w.Write("(WindowsRuntimeActivationTypes.DerivedComposed _, WindowsRuntimeObjectReference activationFactoryObjectReference, in Guid iid, CreateObjectReferenceMarshalingType marshalingType)\n"); - w.Write(" :base(_, activationFactoryObjectReference, in iid, marshalingType)\n"); - w.Write("{\n"); - if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } - w.Write("}\n"); - - // 2. WindowsRuntimeActivationTypes.DerivedSealed - w.Write("\nprotected "); - w.Write(typeName); - w.Write("(WindowsRuntimeActivationTypes.DerivedSealed _, WindowsRuntimeObjectReference activationFactoryObjectReference, in Guid iid, CreateObjectReferenceMarshalingType marshalingType)\n"); - w.Write(" :base(_, activationFactoryObjectReference, in iid, marshalingType)\n"); - w.Write("{\n"); - if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } - w.Write("}\n"); - - // 3. WindowsRuntimeActivationFactoryCallback.DerivedComposed - w.Write("\nprotected "); - w.Write(typeName); - w.Write("(WindowsRuntimeActivationFactoryCallback.DerivedComposed activationFactoryCallback, in Guid iid, CreateObjectReferenceMarshalingType marshalingType, WindowsRuntimeActivationArgsReference additionalParameters)\n"); - w.Write(" :base(activationFactoryCallback, in iid, marshalingType, additionalParameters)\n"); - w.Write("{\n"); - if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } - w.Write("}\n"); - - // 4. WindowsRuntimeActivationFactoryCallback.DerivedSealed - w.Write("\nprotected "); - w.Write(typeName); - w.Write("(WindowsRuntimeActivationFactoryCallback.DerivedSealed activationFactoryCallback, in Guid iid, CreateObjectReferenceMarshalingType marshalingType, WindowsRuntimeActivationArgsReference additionalParameters)\n"); - w.Write(" :base(activationFactoryCallback, in iid, marshalingType, additionalParameters)\n"); - w.Write("{\n"); - if (!string.IsNullOrEmpty(gcPressureBody)) { w.Write(gcPressureBody); } - w.Write("}\n"); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Guids.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Guids.cs deleted file mode 100644 index 9ccba022c2..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Guids.cs +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Globalization; -using System.Text.RegularExpressions; -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// GUID/IID-related code writers, mirroring functions in code_writers.h. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ get_fundamental_type_guid_signature. - public static string GetFundamentalTypeGuidSignature(FundamentalType t) => t switch - { - FundamentalType.Boolean => "b1", - FundamentalType.Char => "c2", - FundamentalType.Int8 => "i1", - FundamentalType.UInt8 => "u1", - FundamentalType.Int16 => "i2", - FundamentalType.UInt16 => "u2", - FundamentalType.Int32 => "i4", - FundamentalType.UInt32 => "u4", - FundamentalType.Int64 => "i8", - FundamentalType.UInt64 => "u8", - FundamentalType.Float => "f4", - FundamentalType.Double => "f8", - FundamentalType.String => "string", - _ => throw new InvalidOperationException("Unknown fundamental type") - }; - - private static readonly Regex s_typeNameEscapeRe = new(@"[ :<>`,.]", RegexOptions.Compiled); - - /// Mirrors C++ escape_type_name_for_identifier. - public static string EscapeTypeNameForIdentifier(string typeName, bool stripGlobal = false, bool stripGlobalABI = false) - { - // Match C++ behavior: escape special chars first, then strip ONLY the prefix (not all - // occurrences). C++ uses rfind(prefix, 0) + erase(0, len) which only removes the prefix. - string result = s_typeNameEscapeRe.Replace(typeName, "_"); - if (stripGlobalABI && typeName.StartsWith("global::ABI.", StringComparison.Ordinal)) - { - result = result.Substring(12); // Remove "global::ABI." (with ":" and "." already replaced) - } - else if (stripGlobal && typeName.StartsWith("global::", StringComparison.Ordinal)) - { - result = result.Substring(8); // Remove "global::" - } - return result; - } - - /// - /// Reads the GUID values from the [GuidAttribute] of the type and returns them as a tuple. - /// - public static (uint Data1, ushort Data2, ushort Data3, byte[] Data4)? GetGuidFields(TypeDefinition type) - { - CustomAttribute? attr = TypeCategorization.GetAttribute(type, "Windows.Foundation.Metadata", "GuidAttribute"); - if (attr is null || attr.Signature is null) { return null; } - var args = attr.Signature.FixedArguments; - if (args.Count < 11) { return null; } - - uint data1 = ToUInt32(args[0].Element); - ushort data2 = ToUInt16(args[1].Element); - ushort data3 = ToUInt16(args[2].Element); - byte[] data4 = new byte[8]; - for (int i = 0; i < 8; i++) - { - data4[i] = ToByte(args[3 + i].Element); - } - return (data1, data2, data3, data4); - - static uint ToUInt32(object? v) => v switch - { - uint u => u, - int i => (uint)i, - _ => 0u - }; - static ushort ToUInt16(object? v) => v switch - { - ushort u => u, - short s => (ushort)s, - int i => (ushort)i, - _ => (ushort)0 - }; - static byte ToByte(object? v) => v switch - { - byte b => b, - sbyte sb => (byte)sb, - int i => (byte)i, - _ => (byte)0 - }; - } - - /// Mirrors C++ write_guid. - public static void WriteGuid(TextWriter w, TypeDefinition type, bool lowerCase) - { - var fields = GetGuidFields(type) ?? throw new InvalidOperationException( - $"'Windows.Foundation.Metadata.GuidAttribute' attribute for type '{type.Namespace}.{type.Name}' not found"); - string fmt = lowerCase ? "x" : "X"; - // Format: %08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x - w.Write(fields.Data1.ToString(fmt + "8", CultureInfo.InvariantCulture)); - w.Write("-"); - w.Write(fields.Data2.ToString(fmt + "4", CultureInfo.InvariantCulture)); - w.Write("-"); - w.Write(fields.Data3.ToString(fmt + "4", CultureInfo.InvariantCulture)); - w.Write("-"); - for (int i = 0; i < 2; i++) { w.Write(fields.Data4[i].ToString(fmt + "2", CultureInfo.InvariantCulture)); } - w.Write("-"); - for (int i = 2; i < 8; i++) { w.Write(fields.Data4[i].ToString(fmt + "2", CultureInfo.InvariantCulture)); } - } - - /// Mirrors C++ write_guid_bytes. - public static void WriteGuidBytes(TextWriter w, TypeDefinition type) - { - var fields = GetGuidFields(type) ?? throw new InvalidOperationException( - $"'Windows.Foundation.Metadata.GuidAttribute' attribute for type '{type.Namespace}.{type.Name}' not found"); - WriteByte(w, (fields.Data1 >> 0) & 0xFF, true); - WriteByte(w, (fields.Data1 >> 8) & 0xFF, false); - WriteByte(w, (fields.Data1 >> 16) & 0xFF, false); - WriteByte(w, (fields.Data1 >> 24) & 0xFF, false); - WriteByte(w, (uint)((fields.Data2 >> 0) & 0xFF), false); - WriteByte(w, (uint)((fields.Data2 >> 8) & 0xFF), false); - WriteByte(w, (uint)((fields.Data3 >> 0) & 0xFF), false); - WriteByte(w, (uint)((fields.Data3 >> 8) & 0xFF), false); - for (int i = 0; i < 8; i++) { WriteByte(w, fields.Data4[i], false); } - } - - private static void WriteByte(TextWriter w, uint b, bool first) - { - if (!first) { w.Write(", "); } - w.Write("0x"); - w.Write((b & 0xFF).ToString("X", CultureInfo.InvariantCulture)); - } - - /// Mirrors C++ write_iid_guid_property_name. - public static void WriteIidGuidPropertyName(TypeWriter w, TypeDefinition type) - { - string name = w.WriteTemp("%", new Action(_ => - { - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - })); - name = EscapeTypeNameForIdentifier(name, true, true); - w.Write("IID_"); - w.Write(name); - } - - /// Mirrors C++ write_iid_reference_guid_property_name. - public static void WriteIidReferenceGuidPropertyName(TypeWriter w, TypeDefinition type) - { - string name = w.WriteTemp("%", new Action(_ => - { - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - })); - name = EscapeTypeNameForIdentifier(name, true, true); - w.Write("IID_"); - w.Write(name); - w.Write("Reference"); - } - - /// Mirrors C++ write_iid_guid_property_from_type. - public static void WriteIidGuidPropertyFromType(TypeWriter w, TypeDefinition type) - { - w.Write("public static ref readonly Guid "); - WriteIidGuidPropertyName(w, type); - w.Write("\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get\n {\n ReadOnlySpan data =\n [\n "); - WriteGuidBytes(w, type); - w.Write("\n ];\n return ref Unsafe.As(ref MemoryMarshal.GetReference(data));\n }\n}\n\n"); - } - - /// - /// Mirrors C++ write_guid_signature. - /// - public static void WriteGuidSignature(TypeWriter w, TypeSemantics semantics) - { - switch (semantics) - { - case TypeSemantics.Guid_: - w.Write("g16"); - break; - case TypeSemantics.Object_: - w.Write("cinterface(IInspectable)"); - break; - case TypeSemantics.Fundamental f: - w.Write(GetFundamentalTypeGuidSignature(f.Type)); - break; - case TypeSemantics.Definition d: - WriteGuidSignatureForType(w, d.Type); - break; - case TypeSemantics.Reference r: - { - // Resolve the reference to a TypeDefinition (cross-module struct field, etc.). - // Mirrors C++ for_typedef which always succeeds in resolving here. - string ns = r.Reference_.Namespace?.Value ?? string.Empty; - string name = r.Reference_.Name?.Value ?? string.Empty; - TypeDefinition? resolved = null; - if (_cacheRef is not null) - { - try { resolved = r.Reference_.Resolve(_cacheRef.RuntimeContext); } - catch { resolved = null; } - resolved ??= _cacheRef.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); - } - if (resolved is not null) - { - WriteGuidSignatureForType(w, resolved); - } - } - break; - case TypeSemantics.GenericInstance gi: - w.Write("pinterface({"); - WriteGuid(w, gi.GenericType, true); - w.Write("};"); - for (int i = 0; i < gi.GenericArgs.Count; i++) - { - if (i > 0) { w.Write(";"); } - WriteGuidSignature(w, gi.GenericArgs[i]); - } - w.Write(")"); - break; - case TypeSemantics.GenericInstanceRef gir: - { - // Cross-module generic instance (e.g. Windows.Foundation.IReference - // appearing as a struct field). Resolve the generic type to a TypeDefinition - // so we can extract its [Guid]; recurse on each type argument. - string ns = gir.GenericType.Namespace?.Value ?? string.Empty; - string name = gir.GenericType.Name?.Value ?? string.Empty; - TypeDefinition? resolved = null; - if (_cacheRef is not null) - { - try { resolved = gir.GenericType.Resolve(_cacheRef.RuntimeContext); } - catch { resolved = null; } - resolved ??= _cacheRef.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); - } - if (resolved is not null) - { - w.Write("pinterface({"); - WriteGuid(w, resolved, true); - w.Write("};"); - for (int i = 0; i < gir.GenericArgs.Count; i++) - { - if (i > 0) { w.Write(";"); } - WriteGuidSignature(w, gir.GenericArgs[i]); - } - w.Write(")"); - } - } - break; - } - } - - private static void WriteGuidSignatureForType(TypeWriter w, TypeDefinition type) - { - TypeCategory cat = TypeCategorization.GetCategory(type); - switch (cat) - { - case TypeCategory.Enum: - w.Write("enum("); - WriteTypedefName(w, type, TypedefNameType.NonProjected, true); - WriteTypeParams(w, type); - w.Write(";"); - w.Write(TypeCategorization.IsFlagsEnum(type) ? "u4" : "i4"); - w.Write(")"); - break; - case TypeCategory.Struct: - w.Write("struct("); - WriteTypedefName(w, type, TypedefNameType.NonProjected, true); - WriteTypeParams(w, type); - w.Write(";"); - bool first = true; - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic) { continue; } - if (field.Signature is null) { continue; } - if (!first) { w.Write(";"); } - first = false; - WriteGuidSignature(w, TypeSemanticsFactory.Get(field.Signature.FieldType)); - } - w.Write(")"); - break; - case TypeCategory.Delegate: - w.Write("delegate({"); - WriteGuid(w, type, true); - w.Write("})"); - break; - case TypeCategory.Interface: - w.Write("{"); - WriteGuid(w, type, true); - w.Write("}"); - break; - case TypeCategory.Class: - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); - if (defaultIface is TypeDefinition di) - { - w.Write("rc("); - WriteTypedefName(w, type, TypedefNameType.NonProjected, true); - WriteTypeParams(w, type); - w.Write(";"); - WriteGuidSignature(w, new TypeSemantics.Definition(di)); - w.Write(")"); - } - else - { - w.Write("{"); - WriteGuid(w, type, true); - w.Write("}"); - } - break; - } - } - - /// Mirrors C++ write_iid_guid_property_from_signature. - public static void WriteIidGuidPropertyFromSignature(TypeWriter w, TypeDefinition type) - { - string guidSig = w.WriteTemp("%", new Action(_ => - { - WriteGuidSignature(w, new TypeSemantics.Definition(type)); - })); - string ireferenceGuidSig = "pinterface({61c17706-2d65-11e0-9ae8-d48564015472};" + guidSig + ")"; - Guid guidValue = GuidGenerator.Generate(ireferenceGuidSig); - byte[] bytes = guidValue.ToByteArray(); - - w.Write("public static ref readonly Guid "); - WriteIidReferenceGuidPropertyName(w, type); - w.Write("\n{\n [MethodImpl(MethodImplOptions.AggressiveInlining)]\n get\n {\n ReadOnlySpan data =\n [\n "); - for (int i = 0; i < 16; i++) - { - if (i > 0) { w.Write(", "); } - w.Write("0x"); - w.Write(bytes[i].ToString("X", CultureInfo.InvariantCulture)); - } - w.Write("\n ];\n return ref Unsafe.As(ref MemoryMarshal.GetReference(data));\n }\n}\n\n"); - } - - /// Mirrors C++ write_iid_guid_property_for_class_interfaces. - public static void WriteIidGuidPropertyForClassInterfaces(TypeWriter w, TypeDefinition type, System.Collections.Generic.HashSet interfacesEmitted) - { - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - // Resolve TypeRef → TypeDefinition via metadata cache (so we pick up cross-module - // inherited interfaces, e.g. Windows.UI.Composition.IAnimationObject from a XAML class). - TypeDefinition? ifaceType = impl.Interface as TypeDefinition; - if (ifaceType is null && impl.Interface is TypeReference tr) - { - string trNs = tr.Namespace?.Value ?? string.Empty; - string trNm = tr.Name?.Value ?? string.Empty; - ifaceType = ResolveCrossModuleType(trNs, trNm); - } - if (ifaceType is null) { continue; } - - string ns = ifaceType.Namespace?.Value ?? string.Empty; - string nm = ifaceType.Name?.Value ?? string.Empty; - // Skip mapped types - if (MappedTypes.Get(ns, nm) is not null) { continue; } - // Skip generic interfaces - if (ifaceType.GenericParameters.Count != 0) { continue; } - // Skip already-emitted - if (interfacesEmitted.Contains(ifaceType)) { continue; } - // Only emit if the interface is not in the projection (otherwise it'll be emitted naturally) - if (!w.Settings.Filter.Includes(ifaceType)) - { - WriteIidGuidPropertyFromType(w, ifaceType); - _ = interfacesEmitted.Add(ifaceType); - } - } - } - - private static TypeDefinition? ResolveCrossModuleType(string ns, string name) - { - if (_cacheRef is null) { return null; } - return _cacheRef.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); - } - - /// Writes the InterfaceIIDs file header (mirrors C++ write_begin_interface_iids in type_writers.h). - public static void WriteInterfaceIidsBegin(TextWriter w) - { - w.Write("\n"); - w.Write("//------------------------------------------------------------------------------\n"); - w.Write("// \n"); - w.Write("// This file was generated by cswinrt.exe version "); - w.Write(GetVersionString()); - w.Write("\n"); - w.Write("//\n"); - w.Write("// Changes to this file may cause incorrect behavior and will be lost if\n"); - w.Write("// the code is regenerated.\n"); - w.Write("// \n"); - w.Write("//------------------------------------------------------------------------------\n"); - w.Write(@" -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace ABI; - -internal static class InterfaceIIDs -{ -"); - } - - /// Writes the InterfaceIIDs file footer. - public static void WriteInterfaceIidsEnd(TextWriter w) - { - w.Write("}\n\n"); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Helpers.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Helpers.cs deleted file mode 100644 index 58e0d13a54..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Helpers.cs +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Helper writers for assembly attributes, metadata attributes, and other infrastructure. -/// Mirrors various functions in code_writers.h. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ write_pragma_disable_IL2026. - public static void WritePragmaDisableIL2026(TextWriter w) - { - w.Write("\n#pragma warning disable IL2026\n"); - } - - /// Mirrors C++ write_pragma_restore_IL2026. - public static void WritePragmaRestoreIL2026(TextWriter w) - { - w.Write("\n#pragma warning restore IL2026\n"); - } - - /// - /// Returns the version string embedded in the banner comment of generated files. - /// Mirrors C++ VERSION_STRING (which is set via $(VersionString) from - /// MSBuild and defaults to 0.0.0-private.0). - /// - /// We read the writer assembly's - /// (set via $(InformationalVersion)) and strip any SourceLink commit-sha suffix - /// after a '+' so the banner is reproducible across rebuilds of the same source. - /// - internal static string GetVersionString() - { - System.Reflection.Assembly asm = typeof(CodeWriters).Assembly; - System.Reflection.AssemblyInformationalVersionAttribute? attr = - (System.Reflection.AssemblyInformationalVersionAttribute?)System.Attribute.GetCustomAttribute( - asm, typeof(System.Reflection.AssemblyInformationalVersionAttribute)); - string version = attr?.InformationalVersion ?? "0.0.0-private.0"; - int plus = version.IndexOf('+'); - return plus >= 0 ? version.Substring(0, plus) : version; - } - - /// Mirrors C++ write_file_header. - public static void WriteFileHeader(TextWriter w) - { - w.Write("//------------------------------------------------------------------------------\n"); - w.Write("// \n"); - w.Write("// This file was generated by cswinrt.exe version "); - w.Write(GetVersionString()); - w.Write("\n"); - w.Write("//\n"); - w.Write("// Changes to this file may cause incorrect behavior and will be lost if\n"); - w.Write("// the code is regenerated.\n"); - w.Write("// \n"); - w.Write("//------------------------------------------------------------------------------\n"); - } - - /// Mirrors C++ write_winrt_metadata_attribute. - public static void WriteWinRTMetadataAttribute(TypeWriter w, TypeDefinition type, MetadataCache cache) - { - string path = cache.GetSourcePath(type); - string stem = string.IsNullOrEmpty(path) ? string.Empty : Path.GetFileNameWithoutExtension(path); - w.Write("[WindowsRuntimeMetadata(\""); - w.Write(stem); - w.Write("\")]\n"); - } - - /// Mirrors C++ write_winrt_metadata_typename_attribute. - public static void WriteWinRTMetadataTypeNameAttribute(TypeWriter w, TypeDefinition type) - { - w.Write("[WindowsRuntimeMetadataTypeName(\""); - WriteTypedefName(w, type, TypedefNameType.NonProjected, true); - WriteTypeParams(w, type); - w.Write("\")]\n"); - } - - /// Mirrors C++ write_winrt_mapped_type_attribute. - public static void WriteWinRTMappedTypeAttribute(TypeWriter w, TypeDefinition type) - { - w.Write("[WindowsRuntimeMappedType(typeof("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - WriteTypeParams(w, type); - w.Write("))]\n"); - } - - /// Mirrors C++ write_value_type_winrt_classname_attribute. - public static void WriteValueTypeWinRTClassNameAttribute(TypeWriter w, TypeDefinition type) - { - if (w.Settings.ReferenceProjection) { return; } - string ns = type.Namespace?.Value ?? string.Empty; - string name = type.Name?.Value ?? string.Empty; - w.Write("[WindowsRuntimeClassName(\"Windows.Foundation.IReference`1<"); - w.Write(ns); - w.Write("."); - w.Write(name); - w.Write(">\")]\n"); - } - - /// Mirrors C++ write_winrt_reference_type_attribute. - public static void WriteWinRTReferenceTypeAttribute(TypeWriter w, TypeDefinition type) - { - if (w.Settings.ReferenceProjection) { return; } - w.Write("[WindowsRuntimeReferenceType(typeof("); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write("?))]\n"); - } - - /// Mirrors C++ write_comwrapper_marshaller_attribute. - public static void WriteComWrapperMarshallerAttribute(TypeWriter w, TypeDefinition type) - { - if (w.Settings.ReferenceProjection) { return; } - string ns = type.Namespace?.Value ?? string.Empty; - string name = type.Name?.Value ?? string.Empty; - w.Write("[ABI."); - w.Write(ns); - w.Write("."); - w.Write(Helpers.StripBackticks(name)); - w.Write("ComWrappersMarshaller]\n"); - } - - /// - /// Mirrors C++ write_winrt_windowsmetadata_typemapgroup_assembly_attribute. - /// - public static void WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(TypeWriter w, TypeDefinition type) - { - // Skip exclusive interfaces and projection-internal interfaces - if (TypeCategorization.GetCategory(type) == TypeCategory.Interface && - (TypeCategorization.IsExclusiveTo(type) || TypeCategorization.IsProjectionInternal(type))) - { - return; - } - - string projectionName = w.WriteTemp("%", new Action(tw => - { - // Use a temporary TypeWriter for the typedef name with full namespace - WriteTypedefName(w, type, TypedefNameType.NonProjected, true); - WriteTypeParams(w, type); - })); - - w.Write("\n[assembly: TypeMap(\n value: \""); - w.Write(projectionName); - w.Write("\",\n target: typeof("); - if (w.Settings.Component) - { - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - } - else - { - w.Write(projectionName); - } - w.Write("),\n trimTarget: typeof("); - w.Write(projectionName); - w.Write("))]\n"); - - if (w.Settings.Component) - { - w.Write("\n[assembly: TypeMapAssociation(\n source: typeof("); - w.Write(projectionName); - w.Write("),\n proxy: typeof("); - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - w.Write("))]\n\n"); - } - } - - /// - /// Mirrors C++ write_winrt_comwrappers_typemapgroup_assembly_attribute. - /// - public static void WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(TypeWriter w, TypeDefinition type, bool isValueType) - { - string projectionName = w.WriteTemp("%", new Action(tw => - { - WriteTypedefName(w, type, TypedefNameType.NonProjected, true); - WriteTypeParams(w, type); - })); - - w.Write("\n[assembly: TypeMap(\n value: \""); - if (isValueType) - { - w.Write("Windows.Foundation.IReference`1<"); - w.Write(projectionName); - w.Write(">"); - } - else - { - w.Write(projectionName); - } - w.Write("\",\n target: typeof("); - if (w.Settings.Component) - { - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - } - else - { - w.Write(projectionName); - } - w.Write("),\n trimTarget: typeof("); - w.Write(projectionName); - w.Write("))]\n"); - - // For non-interface, non-struct authored types, emit proxy association. - TypeCategory cat = TypeCategorization.GetCategory(type); - if (cat != TypeCategory.Interface && cat != TypeCategory.Struct && w.Settings.Component) - { - w.Write("\n[assembly: TypeMapAssociation(\n source: typeof("); - w.Write(projectionName); - w.Write("),\n proxy: typeof("); - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - w.Write("))]\n\n"); - } - } - - /// - /// Mirrors C++ write_winrt_idic_typemapgroup_assembly_attribute. - /// - public static void WriteWinRTIdicTypeMapGroupAssemblyAttribute(TypeWriter w, TypeDefinition type) - { - // Generic interfaces are handled elsewhere - if (type.GenericParameters.Count != 0) { return; } - // Skip exclusive interfaces (unless idic_exclusiveto), and projection-internal - if ((TypeCategorization.IsExclusiveTo(type) && !w.Settings.IdicExclusiveTo) || - TypeCategorization.IsProjectionInternal(type)) - { - return; - } - - w.Write("\n[assembly: TypeMapAssociation(\n source: typeof("); - WriteTypedefName(w, type, TypedefNameType.Projected, true); - WriteTypeParams(w, type); - w.Write("),\n proxy: typeof("); - WriteTypedefName(w, type, TypedefNameType.ABI, true); - WriteTypeParams(w, type); - w.Write("))]\n\n"); - } - - /// - /// Adds an entry to the default-interface map for a class type. - /// Mirrors C++ add_default_interface_entry. - /// - public static void AddDefaultInterfaceEntry(TypeWriter w, TypeDefinition type, System.Collections.Concurrent.ConcurrentDictionary entries) - { - if (w.Settings.ReferenceProjection) { return; } - ITypeDefOrRef? defaultIface = Helpers.GetDefaultInterface(type); - if (defaultIface is null) { return; } - - string typeNs = type.Namespace?.Value ?? string.Empty; - string typeName = type.Name?.Value ?? string.Empty; - string className = $"global::{typeNs}.{Helpers.StripBackticks(typeName)}"; - - // Resolve TypeReference → TypeDefinition so WriteTypeName goes through the Definition - // branch which knows about authored-type CCW namespacing (ABI.Impl. prefix). - ITypeDefOrRef capturedIface = defaultIface; - if (capturedIface is not TypeDefinition && capturedIface is not TypeSpecification && _cacheRef is not null) - { - try - { - TypeDefinition? resolved = capturedIface.Resolve(_cacheRef.RuntimeContext); - if (resolved is not null) { capturedIface = resolved; } - } - catch { /* leave as TypeReference */ } - } - - // Build the interface display name via TypeSemantics so generic instantiations - // (e.g. IDictionary), TypeRefs and TypeDefs are all handled correctly. - // Mirrors C++ 'add_default_interface_entry' which uses 'for_typedef' + 'write_type_name'. - string interfaceName = w.WriteTemp("%", new Action(tw => - { - TypeSemantics semantics = TypeSemanticsFactory.GetFromTypeDefOrRef(capturedIface); - WriteTypeName(w, semantics, TypedefNameType.CCW, true); - })); - - _ = entries.TryAdd(className, interfaceName); - } - - /// - /// Adds entries for [ExclusiveTo] interfaces of the class type. - /// Mirrors C++ add_exclusive_to_interface_entries. - /// - public static void AddExclusiveToInterfaceEntries(TypeWriter w, TypeDefinition type, System.Collections.Concurrent.ConcurrentBag> entries) - { - if (!w.Settings.Component || w.Settings.ReferenceProjection) { return; } - string typeNs = type.Namespace?.Value ?? string.Empty; - string typeName = type.Name?.Value ?? string.Empty; - string className = $"global::{typeNs}.{Helpers.StripBackticks(typeName)}"; - - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - - // Resolve the interface to a TypeDefinition for the [ExclusiveTo] check. - // Mirrors C++ 'for_typedef(get_type_semantics(iface.Interface()))'. - TypeDefinition? ifaceDef = impl.Interface as TypeDefinition; - if (ifaceDef is null && _cacheRef is not null) - { - try { ifaceDef = impl.Interface.Resolve(_cacheRef.RuntimeContext); } - catch { ifaceDef = null; } - } - if (ifaceDef is null && impl.Interface is TypeSpecification spec - && spec.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - ifaceDef = gi.GenericType as TypeDefinition; - if (ifaceDef is null && _cacheRef is not null) - { - try { ifaceDef = gi.GenericType.Resolve(_cacheRef.RuntimeContext); } - catch { ifaceDef = null; } - } - } - if (ifaceDef is null) { continue; } - - if (TypeCategorization.IsExclusiveTo(ifaceDef)) - { - // Resolve TypeReference → TypeDefinition (or TypeSpecification with resolved generic - // type) so WriteTypeName goes through the Definition branch which knows about - // authored-type CCW namespacing (ABI.Impl. prefix). - ITypeDefOrRef capturedIface = impl.Interface; - if (capturedIface is not TypeDefinition && capturedIface is not TypeSpecification && _cacheRef is not null) - { - try - { - TypeDefinition? resolved = capturedIface.Resolve(_cacheRef.RuntimeContext); - if (resolved is not null) { capturedIface = resolved; } - } - catch { /* leave as TypeReference */ } - } - string interfaceName = w.WriteTemp("%", new Action(tw => - { - TypeSemantics semantics = TypeSemanticsFactory.GetFromTypeDefOrRef(capturedIface); - WriteTypeName(w, semantics, TypedefNameType.CCW, true); - })); - entries.Add(new KeyValuePair(className, interfaceName)); - } - } - } - - /// Writes the generated WindowsRuntimeDefaultInterfaces.cs file (mirrors C++ write_default_interfaces_class). - public static void WriteDefaultInterfacesClass(Settings settings, IReadOnlyList> sortedEntries) - { - if (sortedEntries.Count == 0) { return; } - TextWriter w = new(); - WriteFileHeader(w); - w.Write("using System;\nusing WindowsRuntime;\n\n#pragma warning disable CSWINRT3001\n\nnamespace ABI\n{\n"); - foreach (KeyValuePair kv in sortedEntries) - { - w.Write("[WindowsRuntimeDefaultInterface(typeof("); - w.Write(kv.Key); - w.Write("), typeof("); - w.Write(kv.Value); - w.Write("))]\n"); - } - w.Write("internal static class WindowsRuntimeDefaultInterfaces;\n}\n"); - w.FlushToFile(Path.Combine(settings.OutputFolder, "WindowsRuntimeDefaultInterfaces.cs")); - } - - /// Writes the generated WindowsRuntimeExclusiveToInterfaces.cs file (mirrors C++ write_exclusive_to_interfaces_class). - public static void WriteExclusiveToInterfacesClass(Settings settings, IReadOnlyList> sortedEntries) - { - if (sortedEntries.Count == 0) { return; } - TextWriter w = new(); - WriteFileHeader(w); - w.Write("using System;\nusing WindowsRuntime;\n\n#pragma warning disable CSWINRT3001\n\nnamespace ABI\n{\n"); - foreach (KeyValuePair kv in sortedEntries) - { - w.Write("[WindowsRuntimeExclusiveToInterface(typeof("); - w.Write(kv.Key); - w.Write("), typeof("); - w.Write(kv.Value); - w.Write("))]\n"); - } - w.Write("internal static class WindowsRuntimeExclusiveToInterfaces;\n}\n"); - w.FlushToFile(Path.Combine(settings.OutputFolder, "WindowsRuntimeExclusiveToInterfaces.cs")); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Interface.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Interface.cs deleted file mode 100644 index 29647a6682..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Interface.cs +++ /dev/null @@ -1,433 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Interface, class, and ABI emission helpers. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ write_guid_attribute. - public static void WriteGuidAttribute(TypeWriter w, TypeDefinition type) - { - bool fullyQualify = type.Namespace == "Windows.Foundation.Metadata"; - w.Write("["); - w.Write(fullyQualify ? "global::System.Runtime.InteropServices.Guid" : "Guid"); - w.Write("(\""); - WriteGuid(w, type, false); - w.Write("\")]"); - } - - /// Mirrors C++ write_type_inheritance (object base case). - public static void WriteTypeInheritance(TypeWriter w, TypeDefinition type, bool includeExclusiveInterface, bool includeWindowsRuntimeObject) - { - string delimiter = " : "; - - // Check the base type. If the class extends another runtime class (not System.Object), - // emit the projected base type name. Mirrors C++ write_type_inheritance, which only - // checks for object_type — WindowsRuntime.WindowsRuntimeObject is a managed type - // defined in WinRT.Runtime and is never referenced as a base type in any .winmd, so - // there is no need to check for it here. - bool hasNonObjectBase = false; - if (type.BaseType is not null) - { - string? baseNs = type.BaseType.Namespace?.Value; - string? baseName = type.BaseType.Name?.Value; - hasNonObjectBase = !(baseNs == "System" && baseName == "Object"); - } - - if (hasNonObjectBase) - { - w.Write(delimiter); - // Write the projected base type name. Same-namespace types stay unqualified (e.g. - // 'AppointmentActionEntity : ActionEntity') — only emit 'global::' when the base - // class lives in a different namespace (mirrors C++ write_typedef_name behavior). - ITypeDefOrRef baseType = type.BaseType!; - string ns = baseType.Namespace?.Value ?? string.Empty; - string name = baseType.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - if (!string.IsNullOrEmpty(ns) && ns != w.CurrentNamespace) - { - w.Write("global::"); - w.Write(ns); - w.Write("."); - } - w.Write(Helpers.StripBackticks(name)); - delimiter = ", "; - } - else if (includeWindowsRuntimeObject) - { - w.Write(delimiter); - w.Write("WindowsRuntimeObject"); - delimiter = ", "; - } - - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - - bool isOverridable = Helpers.IsOverridable(impl); - - // For TypeDef interfaces, check exclusive_to attribute to decide inclusion. - // For TypeRef interfaces, attempt to resolve via the runtime context. - bool isExclusive = false; - if (impl.Interface is TypeDefinition ifaceTypeDef) - { - isExclusive = TypeCategorization.IsExclusiveTo(ifaceTypeDef); - } - else - { - TypeDefinition? resolved = ResolveInterface(impl.Interface); - if (resolved is not null) - { - isExclusive = TypeCategorization.IsExclusiveTo(resolved); - } - } - - if (!(isOverridable || !isExclusive || includeExclusiveInterface)) - { - continue; - } - - w.Write(delimiter); - delimiter = ", "; - - // Emit the interface name (CCW) with mapped-type remapping - WriteInterfaceTypeName(w, impl.Interface); - - if (includeWindowsRuntimeObject && !w.Settings.ReferenceProjection) - { - w.Write(", IWindowsRuntimeInterface<"); - WriteInterfaceTypeName(w, impl.Interface); - w.Write(">"); - } - } - } - - /// - /// Writes the projected name for an interface reference (TypeDefinition, TypeReference, or - /// generic instance), applying mapped-type remapping (e.g., - /// Windows.Foundation.Collections.IMap<K,V>System.Collections.Generic.IDictionary<K,V>). - /// - public static void WriteInterfaceTypeName(TypeWriter w, ITypeDefOrRef ifaceType) - { - if (ifaceType is TypeDefinition td) - { - WriteTypedefName(w, td, TypedefNameType.CCW, false); - WriteTypeParams(w, td); - } - else if (ifaceType is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - // Only emit the global:: prefix if the namespace doesn't match the current emit namespace - // (mirrors WriteTypedefName behavior — same-namespace types stay unqualified). - if (!string.IsNullOrEmpty(ns) && ns != w.CurrentNamespace) - { - w.Write("global::"); - w.Write(ns); - w.Write("."); - } - w.WriteCode(Helpers.StripBackticks(name)); - } - else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) - { - ITypeDefOrRef gt = gi.GenericType; - string ns = gt.Namespace?.Value ?? string.Empty; - string name = gt.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - if (!string.IsNullOrEmpty(ns) && ns != w.CurrentNamespace) - { - w.Write("global::"); - w.Write(ns); - w.Write("."); - } - w.WriteCode(Helpers.StripBackticks(name)); - w.Write("<"); - for (int i = 0; i < gi.TypeArguments.Count; i++) - { - if (i > 0) { w.Write(", "); } - // Pass forceWriteNamespace=false so type args also respect the current namespace. - WriteTypeName(w, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, false); - } - w.Write(">"); - } - } - - /// Mirrors C++ write_prop_type. - public static string WritePropType(TypeWriter w, PropertyDefinition prop, bool isSetProperty = false) - { - return WritePropType(w, prop, null, isSetProperty); - } - - public static string WritePropType(TypeWriter w, PropertyDefinition prop, AsmResolver.DotNet.Signatures.GenericContext? genCtx, bool isSetProperty = false) - { - TypeSignature? typeSig = prop.Signature?.ReturnType; - if (typeSig is null) { return "object"; } - if (genCtx is not null) { typeSig = typeSig.InstantiateGenericTypes(genCtx.Value); } - return w.WriteTemp("%", new System.Action(_ => WriteProjectedSignature(w, typeSig, isSetProperty))); - } - - /// Mirrors C++ write_interface_member_signatures. - public static void WriteInterfaceMemberSignatures(TypeWriter w, TypeDefinition type) - { - foreach (MethodDefinition method in type.Methods) - { - if (Helpers.IsSpecial(method)) { continue; } - MethodSig sig = new(method); - w.Write("\n"); - // Mirror C++ write_interface_required which calls write_custom_attributes for method.CustomAttribute(). - // Only emit Windows.Foundation.Metadata attributes that have a projected form (Overload, DefaultOverload, AttributeUsage, Experimental). - WriteMethodCustomAttributes(w, method); - WriteProjectionReturnType(w, sig); - w.Write(" "); - w.Write(method.Name?.Value ?? string.Empty); - w.Write("("); - WriteParameterList(w, sig); - w.Write(");"); - } - - foreach (PropertyDefinition prop in type.Properties) - { - (MethodDefinition? getter, MethodDefinition? setter) = Helpers.GetPropertyMethods(prop); - // Mirror C++ code_writers.h:5642 — emit 'new' when the property is setter-only - // on this interface AND a property of the same name exists in any base interface - // (typically the getter-only counterpart). This hides the inherited member. - string newKeyword = (getter is null && setter is not null - && FindPropertyInBaseInterfaces(type, prop.Name?.Value ?? string.Empty)) - ? "new " : string.Empty; - string propType = WritePropType(w, prop); - w.Write("\n"); - w.Write(newKeyword); - w.Write(propType); - w.Write(" "); - w.Write(prop.Name?.Value ?? string.Empty); - w.Write(" {"); - if (getter is not null || setter is not null) { w.Write(" get;"); } - if (setter is not null) { w.Write(" set;"); } - w.Write(" }"); - } - - foreach (EventDefinition evt in type.Events) - { - w.Write("\nevent "); - WriteEventType(w, evt); - w.Write(" "); - w.Write(evt.Name?.Value ?? string.Empty); - w.Write(";"); - } - } - - /// - /// Recursively walks the base interfaces of looking for a property - /// with the given . Mirrors C++ find_property_interface - /// at code_writers.h:4154-4185 (returns true if any base interface declares a property - /// with that name; used to decide whether a setter-only property in a derived interface - /// needs the new modifier to hide the base getter). - /// - private static bool FindPropertyInBaseInterfaces(TypeDefinition type, string propName) - { - if (string.IsNullOrEmpty(propName)) { return false; } - System.Collections.Generic.HashSet visited = new(); - return FindPropertyInBaseInterfacesRecursive(type, propName, visited); - } - - private static bool FindPropertyInBaseInterfacesRecursive(TypeDefinition type, string propName, System.Collections.Generic.HashSet visited) - { - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - TypeDefinition? baseIface = ResolveInterface(impl.Interface); - if (baseIface is null) { continue; } - // Skip the original setter-defining interface itself (matches C++ check - // 'setter_iface != type'). Also dedupe via the visited set. - if (baseIface == type) { continue; } - if (!visited.Add(baseIface)) { continue; } - foreach (PropertyDefinition prop in baseIface.Properties) - { - if ((prop.Name?.Value ?? string.Empty) == propName) { return true; } - } - if (FindPropertyInBaseInterfacesRecursive(baseIface, propName, visited)) { return true; } - } - return false; - } - - /// - /// Like but returns the base interface where the - /// property was found (or null if not found). Mirrors the C++ tool's - /// find_property_interface which returns a pair<TypeDef, bool>. - /// - public static TypeDefinition? FindPropertyInterfaceInBases(TypeDefinition type, string propName) - { - if (string.IsNullOrEmpty(propName)) { return null; } - System.Collections.Generic.HashSet visited = new(); - return FindPropertyInterfaceInBasesRecursive(type, propName, visited); - } - - private static TypeDefinition? FindPropertyInterfaceInBasesRecursive(TypeDefinition type, string propName, System.Collections.Generic.HashSet visited) - { - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - TypeDefinition? baseIface = ResolveInterface(impl.Interface); - if (baseIface is null) { continue; } - if (baseIface == type) { continue; } - if (!visited.Add(baseIface)) { continue; } - foreach (PropertyDefinition prop in baseIface.Properties) - { - if ((prop.Name?.Value ?? string.Empty) == propName) { return baseIface; } - } - TypeDefinition? deeper = FindPropertyInterfaceInBasesRecursive(baseIface, propName, visited); - if (deeper is not null) { return deeper; } - } - return null; - } - - /// - /// Emits the projected custom attributes for an interface method. Mirrors C++ - /// write_custom_attributes filtered for the projected attributes. - /// - private static void WriteMethodCustomAttributes(TypeWriter w, MethodDefinition method) - { - foreach (CustomAttribute attr in method.CustomAttributes) - { - ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; - if (attrType is null) { continue; } - string ns = attrType.Namespace?.Value ?? string.Empty; - string nm = attrType.Name?.Value ?? string.Empty; - if (ns != "Windows.Foundation.Metadata") { continue; } - string baseName = nm.EndsWith("Attribute", System.StringComparison.Ordinal) ? nm[..^"Attribute".Length] : nm; - // Only the attributes the C++ tool considers projected (see code_writers.h). - if (baseName is not ("Overload" or "DefaultOverload" or "Experimental")) - { - continue; - } - w.Write("[global::Windows.Foundation.Metadata."); - w.Write(baseName); - // Args: only handle string args (sufficient for [Overload(@"X")]). [DefaultOverload] has none. - if (attr.Signature is not null && attr.Signature.FixedArguments.Count > 0) - { - w.Write("("); - for (int i = 0; i < attr.Signature.FixedArguments.Count; i++) - { - if (i > 0) { w.Write(", "); } - object? val = attr.Signature.FixedArguments[i].Element; - if (val is AsmResolver.Utf8String s) - { - w.Write("@\""); - w.Write(s.Value); - w.Write("\""); - } - else if (val is string ss) - { - w.Write("@\""); - w.Write(ss); - w.Write("\""); - } - else - { - w.Write(val?.ToString() ?? string.Empty); - } - } - w.Write(")"); - } - w.Write("]\n"); - } - } - - /// - /// Mirrors C++ write_interface. Emits an interface projection. - /// - public static void WriteInterface(TypeWriter w, TypeDefinition type) - { - // Mirrors C++ write_interface skip rule: exclusive interfaces other than the default - // and overridable one are not used in the projection. Skip them unless public_exclusiveto - // is set (or in reference projection or component mode). - if (!w.Settings.ReferenceProjection && - !w.Settings.Component && - TypeCategorization.IsExclusiveTo(type) && - !w.Settings.PublicExclusiveTo && - !IsDefaultOrOverridableInterfaceTypedef(type)) - { - return; - } - - if (w.Settings.Component && !TypeCategorization.IsExclusiveTo(type)) - { - return; - } - - w.Write("\n"); - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteGuidAttribute(w, type); - w.Write("\n"); - WriteTypeCustomAttributes(w, type, false); - - bool isInternal = (TypeCategorization.IsExclusiveTo(type) && !w.Settings.PublicExclusiveTo) || - TypeCategorization.IsProjectionInternal(type); - w.Write(isInternal ? "internal" : "public"); - w.Write(" interface "); - WriteTypedefName(w, type, TypedefNameType.CCW, false); - WriteTypeParams(w, type); - WriteTypeInheritance(w, type, false, false); - w.Write("\n{"); - WriteInterfaceMemberSignatures(w, type); - w.Write("\n}\n"); - } - - /// Mirrors C++ is_default_or_overridable_interface_typedef: returns true if the - /// given exclusive interface is referenced as a [Default] or [Overridable] interface impl on - /// the class it's exclusive to. - private static bool IsDefaultOrOverridableInterfaceTypedef(TypeDefinition iface) - { - if (!TypeCategorization.IsExclusiveTo(iface)) { return false; } - TypeDefinition? classType = GetExclusiveToType(iface); - if (classType is null) { return false; } - foreach (InterfaceImplementation impl in classType.Interfaces) - { - if (!Helpers.IsDefaultInterface(impl) && !Helpers.IsOverridable(impl)) { continue; } - ITypeDefOrRef? implRef = impl.Interface; - if (implRef is null) { continue; } - TypeDefinition? implDef = ResolveInterfaceTypeDefForExclusiveCheck(implRef); - if (implDef is not null && implDef == iface) { return true; } - } - return false; - } - - private static TypeDefinition? ResolveInterfaceTypeDefForExclusiveCheck(ITypeDefOrRef ifaceRef) - { - if (ifaceRef is TypeDefinition td) { return td; } - if (ifaceRef is TypeReference tr && _cacheRef is not null) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string nm = tr.Name?.Value ?? string.Empty; - return _cacheRef.Find(ns + "." + nm); - } - if (ifaceRef is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - ITypeDefOrRef? gen = gi.GenericType; - return gen is null ? null : ResolveInterfaceTypeDefForExclusiveCheck(gen); - } - return null; - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.MappedInterfaceStubs.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.MappedInterfaceStubs.cs deleted file mode 100644 index eab7a4aa99..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.MappedInterfaceStubs.cs +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Emits stub members ('=> throw null!') for well-known C# interfaces that come from mapped -/// WinRT interfaces (IClosable -> IDisposable, IMap`2 -> IDictionary<K,V>, etc.). The -/// runtime adapter actually services these at runtime via IDynamicInterfaceCastable, but the -/// C# compiler still requires the class to declare the members. -/// -internal static partial class CodeWriters -{ - /// - /// Returns true if the WinRT interface (by namespace+name) is a mapped interface that - /// requires emitting C#-interface stub members on the implementing class. - /// - public static bool IsMappedInterfaceRequiringStubs(string ifaceNs, string ifaceName) - { - if (MappedTypes.Get(ifaceNs, ifaceName) is not { HasCustomMembersOutput: true }) - { - return false; - } - return ifaceName switch - { - "IClosable" => true, - "IIterable`1" or "IIterator`1" => true, - "IMap`2" or "IMapView`2" => true, - "IVector`1" or "IVectorView`1" => true, - "IBindableIterable" or "IBindableIterator" or "IBindableVector" => true, - "INotifyDataErrorInfo" => true, - _ => false, - }; - } - - /// - /// Emits the C# interface stub members for the given WinRT interface that maps to a known - /// .NET interface. - /// - /// The writer. - /// The (possibly substituted) generic instance signature for the interface, or null if non-generic. - /// The WinRT interface name (e.g. "IMap`2"). - /// The name of the lazy _objRef_* field for the interface on the class. - public static void WriteMappedInterfaceStubs(TypeWriter w, GenericInstanceTypeSignature? instance, string ifaceName, string objRefName) - { - // Resolve type arguments from the (substituted) generic instance signature, if any. - List typeArgs = new(); - List typeArgSigs = new(); - if (instance is not null) - { - foreach (TypeSignature arg in instance.TypeArguments) - { - typeArgs.Add(TypeSemanticsFactory.Get(arg)); - typeArgSigs.Add(arg); - } - } - - switch (ifaceName) - { - case "IClosable": - EmitDisposable(w, objRefName); - break; - case "IIterable`1": - EmitGenericEnumerable(w, typeArgs, typeArgSigs, objRefName); - break; - case "IIterator`1": - EmitGenericEnumerator(w, typeArgs, typeArgSigs, objRefName); - break; - case "IMap`2": - EmitDictionary(w, typeArgs, typeArgSigs, objRefName); - break; - case "IMapView`2": - EmitReadOnlyDictionary(w, typeArgs, typeArgSigs, objRefName); - break; - case "IVector`1": - EmitList(w, typeArgs, typeArgSigs, objRefName); - break; - case "IVectorView`1": - EmitReadOnlyList(w, typeArgs, typeArgSigs, objRefName); - break; - case "IBindableIterable": - w.Write($"\nIEnumerator global::System.Collections.IEnumerable.GetEnumerator() => global::ABI.System.Collections.IEnumerableMethods.GetEnumerator({objRefName});\n"); - break; - case "IBindableIterator": - w.Write($"\npublic bool MoveNext() => global::ABI.System.Collections.IEnumeratorMethods.MoveNext({objRefName});\n"); - w.Write("public void Reset() => throw new NotSupportedException();\n"); - w.Write($"public object Current => global::ABI.System.Collections.IEnumeratorMethods.Current({objRefName});\n"); - break; - case "IBindableVector": - EmitNonGenericList(w, objRefName); - break; - case "INotifyDataErrorInfo": - w.Write($"\npublic global::System.Collections.IEnumerable GetErrors(string propertyName) => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.GetErrors({objRefName}, propertyName);\n"); - w.Write($"public bool HasErrors {{get => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.HasErrors({objRefName}); }}\n"); - w.Write($"public event global::System.EventHandler ErrorsChanged\n{{\n add => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.ErrorsChanged(this, {objRefName}).Subscribe(value);\n remove => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.ErrorsChanged(this, {objRefName}).Unsubscribe(value);\n}}\n"); - break; - } - } - - private static void EmitDisposable(TypeWriter w, string objRefName) - { - w.Write("\npublic void Dispose() => global::ABI.System.IDisposableMethods.Dispose("); - w.Write(objRefName); - w.Write(");\n"); - } - - private static void EmitGenericEnumerable(TypeWriter w, List args, List argSigs, string objRefName) - { - if (args.Count != 1) { return; } - string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); - string elementId = EncodeArgIdentifier(w, args[0]); - string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); - string interopType = "ABI.System.Collections.Generic.<#corlib>IEnumerable'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; - string prefix = "IEnumerableMethods_" + elementId + "_"; - - w.Write("\n"); - EmitUnsafeAccessor(w, "GetEnumerator", $"IEnumerator<{t}>", $"{prefix}GetEnumerator", interopType, ""); - - w.Write($"\npublic IEnumerator<{t}> GetEnumerator() => {prefix}GetEnumerator(null, {objRefName});\n"); - w.Write("global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();\n"); - } - - private static void EmitGenericEnumerator(TypeWriter w, List args, List argSigs, string objRefName) - { - if (args.Count != 1) { return; } - string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); - string elementId = EncodeArgIdentifier(w, args[0]); - string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); - string interopType = "ABI.System.Collections.Generic.<#corlib>IEnumerator'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; - string prefix = "IEnumeratorMethods_" + elementId + "_"; - - w.Write("\n"); - EmitUnsafeAccessor(w, "Current", t, $"{prefix}Current", interopType, ""); - EmitUnsafeAccessor(w, "MoveNext", "bool", $"{prefix}MoveNext", interopType, ""); - - w.Write($"\npublic bool MoveNext() => {prefix}MoveNext(null, {objRefName});\n"); - w.Write("public void Reset() => throw new NotSupportedException();\n"); - w.Write("public void Dispose() {}\n"); - w.Write($"public {t} Current => {prefix}Current(null, {objRefName});\n"); - w.Write("object global::System.Collections.IEnumerator.Current => Current!;\n"); - } - - private static void EmitDictionary(TypeWriter w, List args, List argSigs, string objRefName) - { - if (args.Count != 2) { return; } - string k = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); - string v = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[1], TypedefNameType.Projected, true))); - // Truth uses two forms for KeyValuePair: - // - 'kv' (unqualified) for plain type usages: parameters, field/return types - // - 'kvNested' (fully qualified) for generic argument usages (inside IEnumerator<>, ICollection<>) - string kv = $"KeyValuePair<{k}, {v}>"; - string kvNested = $"global::System.Collections.Generic.KeyValuePair<{k}, {v}>"; - // Long form (always fully qualified) used for objref field-name computation - // (matches the form WriteClassObjRefDefinitions emits transitively). - string kvLong = kvNested; - string keyId = EncodeArgIdentifier(w, args[0]); - string valId = EncodeArgIdentifier(w, args[1]); - string keyInteropArg = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); - string valInteropArg = EncodeInteropTypeName(argSigs[1], TypedefNameType.Projected); - string interopType = "ABI.System.Collections.Generic.<#corlib>IDictionary'2<" + keyInteropArg + "|" + valInteropArg + ">Methods, WinRT.Interop"; - string prefix = "IDictionaryMethods_" + keyId + "_" + valId + "_"; - // The IEnumerable> objref name (matches what WriteClassObjRefDefinitions emits transitively). - string enumerableObjRefName = "_objRef_System_Collections_Generic_IEnumerable_" + EscapeTypeNameForIdentifier(kvLong, stripGlobal: false) + "_"; - - w.Write("\n"); - EmitUnsafeAccessor(w, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, ""); - EmitUnsafeAccessor(w, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, ""); - EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); - EmitUnsafeAccessor(w, "Item", v, $"{prefix}Item", interopType, $", {k} key"); - EmitUnsafeAccessor(w, "Item", "void", $"{prefix}Item", interopType, $", {k} key, {v} value"); - EmitUnsafeAccessor(w, "Add", "void", $"{prefix}Add", interopType, $", {k} key, {v} value"); - EmitUnsafeAccessor(w, "ContainsKey", "bool", $"{prefix}ContainsKey", interopType, $", {k} key"); - EmitUnsafeAccessor(w, "Remove", "bool", $"{prefix}Remove", interopType, $", {k} key"); - EmitUnsafeAccessor(w, "TryGetValue", "bool", $"{prefix}TryGetValue", interopType, $", {k} key, out {v} value"); - EmitUnsafeAccessor(w, "Add", "void", $"{prefix}Add", interopType, $", {kv} item"); - EmitUnsafeAccessor(w, "Clear", "void", $"{prefix}Clear", interopType, ""); - EmitUnsafeAccessor(w, "Contains", "bool", $"{prefix}Contains", interopType, $", {kv} item"); - EmitUnsafeAccessor(w, "CopyTo", "void", $"{prefix}CopyTo", interopType, $", WindowsRuntimeObjectReference enumObjRef, {kv}[] array, int arrayIndex"); - EmitUnsafeAccessor(w, "Remove", "bool", $"{prefix}Remove", interopType, $", {kv} item"); - - // Public member emission order matches C++ write_dictionary_members_using_static_abi_methods - // (code_writers.h:3677-3694): Keys, Values, Count, IsReadOnly, this[], Add(K,V), - // ContainsKey, Remove(K), TryGetValue, Add(KVP), Clear, Contains, CopyTo, - // ICollection.Remove. WinRT IMap vtable order, NOT alphabetical. - // GetEnumerator is NOT emitted here — it's handled separately by IIterable's - // own EmitGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator - // through write_enumerable_members_using_static_abi_methods). - w.Write($"public ICollection<{k}> Keys => {prefix}Keys(null, {objRefName});\n"); - w.Write($"public ICollection<{v}> Values => {prefix}Values(null, {objRefName});\n"); - w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); - w.Write("public bool IsReadOnly => false;\n"); - w.Write($"public {v} this[{k} key]\n{{\n get => {prefix}Item(null, {objRefName}, key);\n set => {prefix}Item(null, {objRefName}, key, value);\n}}\n"); - w.Write($"public void Add({k} key, {v} value) => {prefix}Add(null, {objRefName}, key, value);\n"); - w.Write($"public bool ContainsKey({k} key) => {prefix}ContainsKey(null, {objRefName}, key);\n"); - w.Write($"public bool Remove({k} key) => {prefix}Remove(null, {objRefName}, key);\n"); - w.Write($"public bool TryGetValue({k} key, out {v} value) => {prefix}TryGetValue(null, {objRefName}, key, out value);\n"); - w.Write($"public void Add({kv} item) => {prefix}Add(null, {objRefName}, item);\n"); - w.Write($"public void Clear() => {prefix}Clear(null, {objRefName});\n"); - w.Write($"public bool Contains({kv} item) => {prefix}Contains(null, {objRefName}, item);\n"); - w.Write($"public void CopyTo({kv}[] array, int arrayIndex) => {prefix}CopyTo(null, {objRefName}, {enumerableObjRefName}, array, arrayIndex);\n"); - // ICollection.Remove must be explicit to avoid clashing with IDictionary.Remove(K key). - w.Write($"bool ICollection<{kv}>.Remove({kv} item) => {prefix}Remove(null, {objRefName}, item);\n"); - } - - private static void EmitReadOnlyDictionary(TypeWriter w, List args, List argSigs, string objRefName) - { - if (args.Count != 2) { return; } - string k = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); - string v = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[1], TypedefNameType.Projected, true))); - string keyId = EncodeArgIdentifier(w, args[0]); - string valId = EncodeArgIdentifier(w, args[1]); - string keyInteropArg = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); - string valInteropArg = EncodeInteropTypeName(argSigs[1], TypedefNameType.Projected); - string interopType = "ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<" + keyInteropArg + "|" + valInteropArg + ">Methods, WinRT.Interop"; - string prefix = "IReadOnlyDictionaryMethods_" + keyId + "_" + valId + "_"; - - w.Write("\n"); - EmitUnsafeAccessor(w, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, ""); - EmitUnsafeAccessor(w, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, ""); - EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); - EmitUnsafeAccessor(w, "Item", v, $"{prefix}Item", interopType, $", {k} key"); - EmitUnsafeAccessor(w, "ContainsKey", "bool", $"{prefix}ContainsKey", interopType, $", {k} key"); - EmitUnsafeAccessor(w, "TryGetValue", "bool", $"{prefix}TryGetValue", interopType, $", {k} key, out {v} value"); - - // GetEnumerator is NOT emitted here — it's handled separately by IIterable's - // EmitGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator - // through write_enumerable_members_using_static_abi_methods). - w.Write($"\npublic {v} this[{k} key] => {prefix}Item(null, {objRefName}, key);\n"); - w.Write($"public IEnumerable<{k}> Keys => {prefix}Keys(null, {objRefName});\n"); - w.Write($"public IEnumerable<{v}> Values => {prefix}Values(null, {objRefName});\n"); - w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); - w.Write($"public bool ContainsKey({k} key) => {prefix}ContainsKey(null, {objRefName}, key);\n"); - w.Write($"public bool TryGetValue({k} key, out {v} value) => {prefix}TryGetValue(null, {objRefName}, key, out value);\n"); - } - - private static void EmitReadOnlyList(TypeWriter w, List args, List argSigs, string objRefName) - { - if (args.Count != 1) { return; } - string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); - string elementId = EncodeArgIdentifier(w, args[0]); - string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); - string interopType = "ABI.System.Collections.Generic.<#corlib>IReadOnlyList'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; - string prefix = "IReadOnlyListMethods_" + elementId + "_"; - - w.Write("\n"); - EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); - EmitUnsafeAccessor(w, "Item", t, $"{prefix}Item", interopType, ", int index"); - - // GetEnumerator is NOT emitted here — it's handled separately by IIterable's - // EmitGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator - // through write_enumerable_members_using_static_abi_methods). - w.Write("\n[global::System.Runtime.CompilerServices.IndexerName(\"ReadOnlyListItem\")]\n"); - w.Write($"public {t} this[int index] => {prefix}Item(null, {objRefName}, index);\n"); - w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); - } - - /// - /// Helper to encode the WinRT.Interop dictionary key for a type-arg encoded identifier - /// (used in UnsafeAccessor function names and method-name prefixes). Mirrors C++ - /// escape_type_name_for_identifier(write_type_name(arg), true). - /// - /// - /// Encodes a type semantics as a C# identifier-safe name. Mirrors C++ - /// escape_type_name_for_identifier(write_projection_type(arg), true): - /// uses the projected type name WITHOUT forcing namespace qualification, then strips - /// 'global::' and replaces '.' with '_'. - /// - private static string EncodeArgIdentifier(TypeWriter w, TypeSemantics arg) - { - string projected = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, arg, TypedefNameType.Projected, false))); - return EscapeTypeNameForIdentifier(projected, stripGlobal: true); - } - - private static void EmitList(TypeWriter w, List args, List argSigs, string objRefName) - { - if (args.Count != 1) { return; } - string t = w.WriteTemp("%", new System.Action(_ => WriteTypeName(w, args[0], TypedefNameType.Projected, true))); - string elementId = EncodeArgIdentifier(w, args[0]); - string interopTypeArgs = EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); - string interopType = "ABI.System.Collections.Generic.<#corlib>IList'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; - string prefix = "IListMethods_" + elementId + "_"; - - w.Write("\n"); - EmitUnsafeAccessor(w, "Count", "int", $"{prefix}Count", interopType, ""); - EmitUnsafeAccessor(w, "Item", t, $"{prefix}Item", interopType, ", int index"); - EmitUnsafeAccessor(w, "Item", "void", $"{prefix}Item", interopType, $", int index, {t} value"); - EmitUnsafeAccessor(w, "IndexOf", "int", $"{prefix}IndexOf", interopType, $", {t} item"); - EmitUnsafeAccessor(w, "Insert", "void", $"{prefix}Insert", interopType, $", int index, {t} item"); - EmitUnsafeAccessor(w, "RemoveAt", "void", $"{prefix}RemoveAt", interopType, ", int index"); - EmitUnsafeAccessor(w, "Add", "void", $"{prefix}Add", interopType, $", {t} item"); - EmitUnsafeAccessor(w, "Clear", "void", $"{prefix}Clear", interopType, ""); - EmitUnsafeAccessor(w, "Contains", "bool", $"{prefix}Contains", interopType, $", {t} item"); - EmitUnsafeAccessor(w, "CopyTo", "void", $"{prefix}CopyTo", interopType, $", {t}[] array, int arrayIndex"); - EmitUnsafeAccessor(w, "Remove", "bool", $"{prefix}Remove", interopType, $", {t} item"); - - // Public member emission order matches C++ write_list_members_using_static_abi_methods - // (code_writers.h:4017-4046): Count, IsReadOnly, this[], IndexOf, Insert, RemoveAt, - // Add, Clear, Contains, CopyTo, Remove. This is the WinRT IVector vtable order - // mapped to IList, NOT alphabetical. GetEnumerator is NOT emitted here — it's - // handled separately by IIterable's own EmitGenericEnumerable invocation - // (mirrors C++ which only emits GetEnumerator through write_enumerable_members_using_static_abi_methods). - w.Write($"public int Count => {prefix}Count(null, {objRefName});\n"); - w.Write("public bool IsReadOnly => false;\n"); - w.Write("\n[global::System.Runtime.CompilerServices.IndexerName(\"ListItem\")]\n"); - w.Write($"public {t} this[int index]\n{{\n get => {prefix}Item(null, {objRefName}, index);\n set => {prefix}Item(null, {objRefName}, index, value);\n}}\n"); - w.Write($"public int IndexOf({t} item) => {prefix}IndexOf(null, {objRefName}, item);\n"); - w.Write($"public void Insert(int index, {t} item) => {prefix}Insert(null, {objRefName}, index, item);\n"); - w.Write($"public void RemoveAt(int index) => {prefix}RemoveAt(null, {objRefName}, index);\n"); - w.Write($"public void Add({t} item) => {prefix}Add(null, {objRefName}, item);\n"); - w.Write($"public void Clear() => {prefix}Clear(null, {objRefName});\n"); - w.Write($"public bool Contains({t} item) => {prefix}Contains(null, {objRefName}, item);\n"); - w.Write($"public void CopyTo({t}[] array, int arrayIndex) => {prefix}CopyTo(null, {objRefName}, array, arrayIndex);\n"); - w.Write($"public bool Remove({t} item) => {prefix}Remove(null, {objRefName}, item);\n"); - } - - /// - /// Emits a single [UnsafeAccessor] static extern declaration that targets a method on a - /// WinRT.Interop helper type. The function signature is built from the supplied parts. - /// - private static void EmitUnsafeAccessor(TypeWriter w, string accessName, string returnType, string functionName, string interopType, string extraParams) - { - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \""); - w.Write(accessName); - w.Write("\")]\n"); - w.Write("static extern "); - w.Write(returnType); - w.Write(" "); - w.Write(functionName); - w.Write("([UnsafeAccessorType(\""); - w.Write(interopType); - w.Write("\")] object _, WindowsRuntimeObjectReference objRef"); - w.Write(extraParams); - w.Write(");\n\n"); - } - - private static void EmitNonGenericList(TypeWriter w, string objRefName) - { - w.Write("\n[global::System.Runtime.CompilerServices.IndexerName(\"NonGenericListItem\")]\n"); - w.Write($"public object this[int index]\n{{\n get => global::ABI.System.Collections.IListMethods.Item({objRefName}, index);\n set => global::ABI.System.Collections.IListMethods.Item({objRefName}, index, value);\n}}\n"); - w.Write($"public int Count => global::ABI.System.Collections.IListMethods.Count({objRefName});\n"); - w.Write("public bool IsReadOnly => false;\n"); - w.Write("public bool IsFixedSize => false;\n"); - w.Write("public bool IsSynchronized => false;\n"); - w.Write("public object SyncRoot => this;\n"); - w.Write($"public int Add(object value) => global::ABI.System.Collections.IListMethods.Add({objRefName}, value);\n"); - w.Write($"public void Clear() => global::ABI.System.Collections.IListMethods.Clear({objRefName});\n"); - w.Write($"public bool Contains(object value) => global::ABI.System.Collections.IListMethods.Contains({objRefName}, value);\n"); - w.Write($"public int IndexOf(object value) => global::ABI.System.Collections.IListMethods.IndexOf({objRefName}, value);\n"); - w.Write($"public void Insert(int index, object value) => global::ABI.System.Collections.IListMethods.Insert({objRefName}, index, value);\n"); - w.Write($"public void Remove(object value) => global::ABI.System.Collections.IListMethods.Remove({objRefName}, value);\n"); - w.Write($"public void RemoveAt(int index) => global::ABI.System.Collections.IListMethods.RemoveAt({objRefName}, index);\n"); - w.Write($"public void CopyTo(Array array, int index) => global::ABI.System.Collections.IListMethods.CopyTo({objRefName}, array, index);\n"); - // GetEnumerator is NOT emitted here — it's handled separately by IBindableIterable's - // EmitNonGenericEnumerable invocation (mirrors C++ which only emits GetEnumerator - // through write_nongeneric_enumerable_members_using_static_abi_methods). - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Methods.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Methods.cs deleted file mode 100644 index c13bc1033b..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.Methods.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Helpers for method/parameter/return type emission. Mirrors various functions in code_writers.h. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ write_projected_signature. - public static void WriteProjectedSignature(TypeWriter w, TypeSignature typeSig, bool isParameter) - { - // Detect SZ-array - if (typeSig is SzArrayTypeSignature sz) - { - // Mirrors C++ write_projected_signature (code_writers.h:822-834): for parameters, - // SZ arrays project as ReadOnlySpan (matches the property setter parameter - // convention; pass_array semantics). - if (isParameter) - { - w.Write("ReadOnlySpan<"); - WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)); - w.Write(">"); - } - else - { - WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)); - w.Write("[]"); - } - return; - } - if (typeSig is ByReferenceTypeSignature br) - { - WriteProjectionType(w, TypeSemanticsFactory.Get(br.BaseType)); - return; - } - WriteProjectionType(w, TypeSemanticsFactory.Get(typeSig)); - } - - /// Mirrors C++ write_projection_parameter_type. - public static void WriteProjectionParameterType(TypeWriter w, ParamInfo p) - { - ParamCategory cat = ParamHelpers.GetParamCategory(p); - switch (cat) - { - case ParamCategory.Out: - w.Write("out "); - WriteProjectedSignature(w, p.Type, true); - break; - case ParamCategory.Ref: - w.Write("in "); - WriteProjectedSignature(w, p.Type, true); - break; - case ParamCategory.PassArray: - w.Write("ReadOnlySpan<"); - WriteProjectionType(w, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); - w.Write(">"); - break; - case ParamCategory.FillArray: - w.Write("Span<"); - WriteProjectionType(w, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); - w.Write(">"); - break; - case ParamCategory.ReceiveArray: - w.Write("out "); - { - SzArrayTypeSignature? sz = p.Type as SzArrayTypeSignature - ?? (p.Type is ByReferenceTypeSignature br ? br.BaseType as SzArrayTypeSignature : null); - if (sz is not null) - { - WriteProjectionType(w, TypeSemanticsFactory.Get(sz.BaseType)); - w.Write("[]"); - } - else - { - WriteProjectedSignature(w, p.Type, true); - } - } - break; - default: - WriteProjectedSignature(w, p.Type, true); - break; - } - } - - /// Mirrors C++ write_parameter_name. - public static void WriteParameterName(TypeWriter w, ParamInfo p) - { - string name = p.Parameter.Name ?? "param"; - Helpers.WriteEscapedIdentifier(w, name); - } - - /// Mirrors C++ write_projection_parameter. - public static void WriteProjectionParameter(TypeWriter w, ParamInfo p) - { - WriteProjectionParameterType(w, p); - w.Write(" "); - WriteParameterName(w, p); - } - - /// Mirrors C++ write_projection_return_type. - public static void WriteProjectionReturnType(TypeWriter w, MethodSig sig) - { - TypeSignature? rt = sig.ReturnType; - if (rt is null) - { - w.Write("void"); - return; - } - WriteProjectedSignature(w, rt, false); - } - - /// Writes a parameter list separated by commas. - public static void WriteParameterList(TypeWriter w, MethodSig sig) - { - for (int i = 0; i < sig.Params.Count; i++) - { - if (i > 0) { w.Write(", "); } - WriteProjectionParameter(w, sig.Params[i]); - } - } - - /// Writes a constant value as a C# literal. Mirrors C++ write_constant partially. - public static string FormatField(FieldDefinition field) - { - if (field.Constant is null) { return string.Empty; } - return FormatConstant(field.Constant); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ObjRefs.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ObjRefs.cs deleted file mode 100644 index 4b527d9be8..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.ObjRefs.cs +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// ObjRef field emission for runtime classes (mirrors C++ write_class_objrefs_definition -/// and helpers around write_objref_type_name / write_iid_guid). -/// -internal static partial class CodeWriters -{ - /// - /// Returns the field name for the given interface impl (e.g. _objRef_System_IDisposable). - /// Mirrors C++ write_objref_type_name: takes the projected interface name (with the - /// namespace forcibly included), strips the global:: prefix and replaces - /// non-identifier characters with _. - /// - public static string GetObjRefName(TypeWriter w, ITypeDefOrRef ifaceType) - { - // Build the projected, fully-qualified name with global::. - string projected; - if (ifaceType is TypeDefinition td) - { - string ns = td.Namespace?.Value ?? string.Empty; - string name = td.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - projected = "global::" + ns + "." + Helpers.StripBackticks(name); - } - else if (ifaceType is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - projected = "global::" + ns + "." + Helpers.StripBackticks(name); - } - else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) - { - // Generic instantiation: always use fully qualified name (with global::) for the objref - // name computation, so the resulting field name is unique across namespaces. This - // matches truth output: e.g. _objRef_System_Collections_Generic_IReadOnlyList_global__Windows_Web_Http_HttpCookie_ - // (with 'global__' coming from escaping the global:: prefix). - projected = w.WriteTemp("%", new Action(_ => - { - WriteFullyQualifiedInterfaceName(w, ifaceType); - })); - } - else - { - projected = w.WriteTemp("%", new Action(_ => - { - WriteFullyQualifiedInterfaceName(w, ifaceType); - })); - } - return "_objRef_" + EscapeTypeNameForIdentifier(projected, stripGlobal: true); - } - - /// - /// Like but always emits a fully qualified name with - /// global:: prefix on every type (even same-namespace ones). Used for objref name - /// computation where uniqueness across namespaces matters. - /// - private static void WriteFullyQualifiedInterfaceName(TypeWriter w, ITypeDefOrRef ifaceType) - { - if (ifaceType is TypeDefinition td) - { - string ns = td.Namespace?.Value ?? string.Empty; - string name = td.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - w.Write("global::"); - if (!string.IsNullOrEmpty(ns)) { w.Write(ns); w.Write("."); } - w.WriteCode(Helpers.StripBackticks(name)); - } - else if (ifaceType is TypeReference tr) - { - string ns = tr.Namespace?.Value ?? string.Empty; - string name = tr.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - w.Write("global::"); - if (!string.IsNullOrEmpty(ns)) { w.Write(ns); w.Write("."); } - w.WriteCode(Helpers.StripBackticks(name)); - } - else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) - { - ITypeDefOrRef gt = gi.GenericType; - string ns = gt.Namespace?.Value ?? string.Empty; - string name = gt.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - w.Write("global::"); - if (!string.IsNullOrEmpty(ns)) { w.Write(ns); w.Write("."); } - w.WriteCode(Helpers.StripBackticks(name)); - w.Write("<"); - for (int i = 0; i < gi.TypeArguments.Count; i++) - { - if (i > 0) { w.Write(", "); } - // forceWriteNamespace=true so generic args also get global:: prefix. - WriteTypeName(w, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, true); - } - w.Write(">"); - } - } - - /// - /// Writes the IID expression for the given interface impl (used as the second arg to - /// NativeObjectReference.As(...)). Mirrors C++ write_iid_guid. - /// - public static void WriteIidExpression(TypeWriter w, ITypeDefOrRef ifaceType) - { - // Generic instantiation: use the UnsafeAccessor extern method declared above the field - // (e.g. IID_Windows_Foundation_Collections_IObservableMap_string__object_(null)). - if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) - { - string propName = BuildIidPropertyNameForGenericInterface(w, gi); - w.Write(propName); - w.Write("(null)"); - return; - } - - string ns; - string name; - bool isMapped; - if (ifaceType is TypeDefinition td) - { - ns = td.Namespace?.Value ?? string.Empty; - name = td.Name?.Value ?? string.Empty; - isMapped = MappedTypes.Get(ns, name) is not null; - } - else if (ifaceType is TypeReference tr) - { - ns = tr.Namespace?.Value ?? string.Empty; - name = tr.Name?.Value ?? string.Empty; - isMapped = MappedTypes.Get(ns, name) is not null; - } - else - { - w.Write("default(global::System.Guid)"); - return; - } - - if (isMapped) - { - // IStringable maps to a simpler IID name in WellKnownInterfaceIIDs. - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is { MappedName: "IStringable" }) - { - w.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable"); - return; - } - // Mapped interface: use WellKnownInterfaceIIDs.IID_. - // The non-projected name is the original WinRT interface (e.g. "Windows.Foundation.IClosable"). - string id = EscapeIdentifier(ns + "." + Helpers.StripBackticks(name)); - w.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_"); - w.Write(id); - } - else - { - // Non-mapped, non-generic: ABI.InterfaceIIDs.IID_. - // Uses the "ABI." prefix on the namespace, escaped with stripGlobalABI. - string abiQualified = "global::ABI." + ns + "." + Helpers.StripBackticks(name); - string id = EscapeTypeNameForIdentifier(abiQualified, stripGlobal: false, stripGlobalABI: true); - w.Write("global::ABI.InterfaceIIDs.IID_"); - w.Write(id); - } - } - - /// - /// Builds the IID property name for a generic interface instantiation. Mirrors C++ - /// write_iid_guid_property_name: write_type_name(type, ABI, true) + escape. - /// E.g. IObservableMap<string, object> -> IID_Windows_Foundation_Collections_IObservableMap_string__object_. - /// - private static string BuildIidPropertyNameForGenericInterface(TypeWriter w, GenericInstanceTypeSignature gi) - { - TypeSemantics sem = TypeSemanticsFactory.Get(gi); - string name = w.WriteTemp("%", new System.Action(_ => - { - WriteTypeName(w, sem, TypedefNameType.ABI, forceWriteNamespace: true); - })); - return "IID_" + EscapeTypeNameForIdentifier(name, stripGlobal: true, stripGlobalABI: true); - } - - /// - /// Emits the [UnsafeAccessor] extern method declaration that exposes the IID for a generic - /// interface instantiation. Mirrors C++ write_unsafe_accessor_for_iid. - /// - /// When true, the accessor's parameter type is - /// object? (used inside #nullable enable regions); otherwise object. - private static void EmitUnsafeAccessorForIid(TypeWriter w, GenericInstanceTypeSignature gi, bool isInNullableContext = false) - { - string propName = BuildIidPropertyNameForGenericInterface(w, gi); - string interopName = EncodeInteropTypeName(gi, TypedefNameType.InteropIID); - w.Write("[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = \"get_IID_"); - w.Write(interopName); - w.Write("\")]\n"); - w.Write("static extern ref readonly Guid "); - w.Write(propName); - w.Write("([UnsafeAccessorType(\"ABI.InterfaceIIDs, WinRT.Interop\")] object"); - if (isInNullableContext) { w.Write("?"); } - w.Write(" _);\n"); - } - - private static string EscapeIdentifier(string s) - { - System.Text.StringBuilder sb = new(s.Length); - foreach (char c in s) - { - sb.Append((c == ' ' || c == ':' || c == '<' || c == '>' || c == '`' || c == ',' || c == '.') ? '_' : c); - } - return sb.ToString(); - } - - /// - /// Writes the IReference<T> IID expression for a value type (used by BoxToUnmanaged). - /// Mirrors the C++ output: global::ABI.InterfaceIIDs.IID_<EscapedABIName>Reference. - /// - public static void WriteIidReferenceExpression(TypeWriter w, TypeDefinition type) - { - string ns = type.Namespace?.Value ?? string.Empty; - string name = type.Name?.Value ?? string.Empty; - string abiQualified = "global::ABI." + ns + "." + Helpers.StripBackticks(name); - string id = EscapeTypeNameForIdentifier(abiQualified, stripGlobal: false, stripGlobalABI: true); - w.Write("global::ABI.InterfaceIIDs.IID_"); - w.Write(id); - w.Write("Reference"); - } - - /// - /// Emits the lazy _objRef_* field definitions for each interface implementation on - /// the given runtime class (mirrors C++ write_class_objrefs_definition). - /// The C++ uses replaceDefaultByInner = type.Flags().Sealed(): for sealed classes, - /// the default interface is emitted as a simple => NativeObjectReference expression; - /// for unsealed classes, ALL interfaces (including the default) use the lazy - /// MakeObjectReference pattern, and the default also gets an init; accessor so the - /// constructor can set it via _objRef_X = NativeObjectReference. - /// - public static void WriteClassObjRefDefinitions(TypeWriter w, TypeDefinition type) - { - // Per-interface _objRef_* getters are emitted in BOTH impl and ref modes with full - // bodies. C++ write_class_objrefs_definition has no settings.reference_projection - // gate. Truth ref-mode output keeps the full Interlocked.CompareExchange + - // NativeObjectReference.As(IID_X(null)) lazy-init bodies. (Only the static factory - // _objRef_* getters become `throw null;` in ref mode — see WriteStaticFactoryObjRef - // and WriteAttributedTypes.) - - // Track names emitted so we don't emit duplicates (e.g. when both IFoo and IFoo2 - // produce the same _objRef_). - HashSet emitted = new(System.StringComparer.Ordinal); - bool isSealed = type.IsSealed; - - // Pass 1: emit objrefs for ALL directly-declared interfaces first (in InterfaceImpl - // declaration order). Pass 2 then walks transitive parents to cover any not yet emitted. - // This mirrors C++ which emits all direct impls first before recursing — so for a class - // that declares 'IFoo, IBar' where IFoo : IBaz, the order is IFoo, IBar, IBaz, NOT - // IFoo, IBaz, IBar. - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - if (!IsInterfaceInInheritanceList(impl, includeExclusiveInterface: false) - && !IsInterfaceForObjRef(impl)) - { - continue; - } - - // Mirrors C++ write_class_objrefs_definition (code_writers.h:2960): for fast-abi - // classes, skip non-default exclusive interfaces — their methods dispatch through - // the default interface's vtable so a separate objref is unnecessary. - bool isDefault = TypeCategorization.HasAttribute(impl, "Windows.Foundation.Metadata", "DefaultAttribute"); - if (!isDefault && IsFastAbiClass(type, w.Settings)) - { - TypeDefinition? implTypeDef = ResolveInterfaceTypeDef(impl.Interface); - if (implTypeDef is not null && TypeCategorization.IsExclusiveTo(implTypeDef)) - { - continue; - } - } - - EmitObjRefForInterface(w, impl.Interface, emitted, isDefault: isDefault, useSimplePattern: isSealed && isDefault); - } - foreach (InterfaceImplementation impl in type.Interfaces) - { - if (impl.Interface is null) { continue; } - if (!IsInterfaceInInheritanceList(impl, includeExclusiveInterface: false) - && !IsInterfaceForObjRef(impl)) - { - continue; - } - // Same fast-abi guard as the first pass. - bool isDefault2 = TypeCategorization.HasAttribute(impl, "Windows.Foundation.Metadata", "DefaultAttribute"); - if (!isDefault2 && IsFastAbiClass(type, w.Settings)) - { - TypeDefinition? implTypeDef = ResolveInterfaceTypeDef(impl.Interface); - if (implTypeDef is not null && TypeCategorization.IsExclusiveTo(implTypeDef)) - { - continue; - } - } - EmitTransitiveInterfaceObjRefs(w, impl.Interface, emitted); - } - } - - /// Emits an _objRef_ field for a single interface impl reference. - /// When true, emit the simple expression-bodied form - /// => NativeObjectReference. Otherwise emit the lazy MakeObjectReference pattern. - private static void EmitObjRefForInterface(TypeWriter w, ITypeDefOrRef ifaceRef, HashSet emitted, bool isDefault, bool useSimplePattern = false) - { - string objRefName = GetObjRefName(w, ifaceRef); - if (!emitted.Add(objRefName)) { return; } - - // Mirrors C++ write_class_objrefs_definition: for generic interface instantiations, emit - // the [UnsafeAccessor] extern method declaration (used by the IID expression in both - // simple and lazy patterns). - bool isGenericInstance = ifaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature; - GenericInstanceTypeSignature? gi = isGenericInstance - ? (GenericInstanceTypeSignature)((TypeSpecification)ifaceRef).Signature! - : null; - - if (useSimplePattern) - { - // Sealed-class default interface: simple expression-bodied property pointing at NativeObjectReference. - w.Write("private WindowsRuntimeObjectReference "); - w.Write(objRefName); - w.Write(" => NativeObjectReference;\n"); - // Emit the unsafe accessor AFTER the field so it can be used to pass the IID in the - // constructor for the default interface (mirrors C++ ordering at line ~3018-3022). - if (gi is not null) - { - EmitUnsafeAccessorForIid(w, gi); - } - } - else - { - // Emit the unsafe accessor BEFORE the lazy field so it's referenced inside the As(...) call. - if (gi is not null) - { - EmitUnsafeAccessorForIid(w, gi); - } - // Lazy CompareExchange pattern. For unsealed-class defaults, also emit 'init;' so the - // constructor can assign NativeObjectReference for the exact-type case. - w.Write("private WindowsRuntimeObjectReference "); - w.Write(objRefName); - w.Write("\n{\n"); - w.Write(" get\n {\n"); - w.Write(" [MethodImpl(MethodImplOptions.NoInlining)]\n"); - w.Write(" WindowsRuntimeObjectReference MakeObjectReference()\n {\n"); - w.Write(" _ = global::System.Threading.Interlocked.CompareExchange(\n"); - w.Write(" location1: ref field,\n"); - w.Write(" value: NativeObjectReference.As("); - WriteIidExpression(w, ifaceRef); - w.Write("),\n"); - w.Write(" comparand: null);\n\n"); - w.Write(" return field;\n }\n\n"); - w.Write(" return field ?? MakeObjectReference();\n }\n"); - if (isDefault) { w.Write(" init;\n"); } - w.Write("}\n"); - } - } - - /// - /// Walks transitively-inherited interfaces and emits an objref field for each one. Mirrors - /// the recursive interface walk needed for mapped collection dispatch. - /// - private static void EmitTransitiveInterfaceObjRefs(TypeWriter w, ITypeDefOrRef ifaceRef, HashSet emitted) - { - // Resolve the interface to its TypeDefinition; if cross-module, look it up in the cache. - TypeDefinition? ifaceTd = ResolveInterfaceTypeDef(ifaceRef); - if (ifaceTd is null) { return; } - - // Compute a substitution context if the parent is a closed generic instance. - AsmResolver.DotNet.Signatures.GenericContext? ctx = null; - if (ifaceRef is TypeSpecification ts && ts.Signature is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi) - { - ctx = new AsmResolver.DotNet.Signatures.GenericContext(gi, null); - } - - foreach (InterfaceImplementation childImpl in ifaceTd.Interfaces) - { - if (childImpl.Interface is null) { continue; } - - // If the parent is a closed generic, substitute the child's signature. - ITypeDefOrRef childRef = childImpl.Interface; - if (ctx is not null) - { - AsmResolver.DotNet.Signatures.TypeSignature childSig = childRef.ToTypeSignature(false); - AsmResolver.DotNet.Signatures.TypeSignature substitutedSig = childSig.InstantiateGenericTypes(ctx.Value); - AsmResolver.DotNet.ITypeDefOrRef? newRef = substitutedSig.ToTypeDefOrRef(); - if (newRef is not null) { childRef = newRef; } - } - - // Skip exclusive-to-someone-else interfaces. Mirrors EmitImplType-like check. - // For now, just emit (no-op if exclusive — the field still works for QI lookup). - EmitObjRefForInterface(w, childRef, emitted, isDefault: false); - EmitTransitiveInterfaceObjRefs(w, childRef, emitted); - } - } - - /// - /// Whether this interface impl needs an _objRef_* field even though it isn't part of the - /// inheritance list (e.g. ExclusiveTo interfaces still need their objref since instance - /// methods/properties dispatch through it). - /// - public static bool IsInterfaceForObjRef(InterfaceImplementation impl) - { - // For now, emit objrefs for ALL implemented interfaces — instance member dispatch - // needs to be able to reach them. - return impl.Interface is not null; - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.RefModeStubs.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.RefModeStubs.cs deleted file mode 100644 index ff0414f77e..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.RefModeStubs.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Reference-projection stub emission helpers. In reference projection mode, all method/property/ -/// event bodies (and certain other constructs like static factory objref getters, activation -/// factory objref getters, and the synthetic private ctor for classes without explicit -/// constructors) collapse to throw null. Mirrors C++ code_writers.h:1639/1655/1671/ -/// 1685/1699/1713/2755/2796/2217/2240/6851/9536. -/// -internal static partial class CodeWriters -{ - /// - /// Emits the body of an _objRef_* property getter in reference projection mode. - /// Mirrors C++ write_static_objref_definition / write_activation_factory_objref_definition - /// (code_writers.h:2755 and 2796) which emit { get { throw null; } } in ref mode. - /// - public static void EmitRefModeObjRefGetterBody(TypeWriter w) - { - w.Write("\n{\n get\n {\n throw null;\n }\n}\n"); - } - - /// - /// Emits the synthetic private TypeName() { throw null; } ctor used in reference - /// projection mode to suppress the C# compiler's implicit public default constructor when - /// no explicit ctors are emitted by WriteAttributedTypes. - /// Mirrors C++ code_writers.h:9536. - /// - public static void EmitSyntheticPrivateCtor(TypeWriter w, string typeName) - { - w.Write("\nprivate "); - w.Write(typeName); - w.Write("() { throw null; }\n"); - } - - /// - /// Emits the body of a delegate factory Invoke method in reference projection mode. - /// Mirrors C++ code_writers.h:6851 which emits throw null; for the activator - /// factory delegate's Invoke body in ref mode. - /// - public static void EmitRefModeInvokeBody(TypeWriter w) - { - w.Write(" throw null;\n }\n}\n"); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.TypeNames.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.TypeNames.cs deleted file mode 100644 index b602caefef..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.TypeNames.cs +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Type-name emission helpers, mirroring C++ code_writers.h. -/// -internal static partial class CodeWriters -{ - /// Mirrors C++ write_fundamental_type. - public static void WriteFundamentalType(TextWriter w, FundamentalType t) - { - w.Write(FundamentalTypes.ToCSharpType(t)); - } - - /// Mirrors C++ write_fundamental_non_projected_type. - public static void WriteFundamentalNonProjectedType(TextWriter w, FundamentalType t) - { - w.Write(FundamentalTypes.ToDotNetType(t)); - } - - /// Mirrors C++ write_typedef_name: writes the C# type name for a typed reference. - public static void WriteTypedefName(TypeWriter w, TypeDefinition type, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) - { - bool authoredType = w.Settings.Component && w.Settings.Filter.Includes(type); - string typeNamespace = type.Namespace?.Value ?? string.Empty; - string typeName = type.Name?.Value ?? string.Empty; - - if (nameType == TypedefNameType.NonProjected) - { - w.Write(typeNamespace); - w.Write("."); - w.Write(typeName); - return; - } - - MappedType? proj = MappedTypes.Get(typeNamespace, typeName); - if (proj is not null) - { - typeNamespace = proj.MappedNamespace; - typeName = proj.MappedName; - } - - // Exclusive interfaces handling: simplified port — we don't try to resolve exclusive_to_type from - // attributes here. Only used in component mode which we don't fully implement here yet. - TypedefNameType nameToWrite = nameType; - if (authoredType && TypeCategorization.IsExclusiveTo(type) && nameToWrite == TypedefNameType.Projected) - { - // Fallback: switch to CCW if the type is not the default interface for its exclusive class. - nameToWrite = TypedefNameType.CCW; - } - - // Authored interfaces that aren't exclusive use the same authored interface. - if (authoredType && nameToWrite == TypedefNameType.CCW && - TypeCategorization.GetCategory(type) == TypeCategory.Interface && - !TypeCategorization.IsExclusiveTo(type)) - { - nameToWrite = TypedefNameType.Projected; - } - - if (nameToWrite == TypedefNameType.EventSource && typeNamespace == "System") - { - w.Write("global::WindowsRuntime.InteropServices."); - } - else if (forceWriteNamespace || - typeNamespace != w.CurrentNamespace || - (nameToWrite == TypedefNameType.Projected && (w.InAbiNamespace || w.InAbiImplNamespace)) || - (nameToWrite == TypedefNameType.ABI && !w.InAbiNamespace) || - (nameToWrite == TypedefNameType.EventSource && !w.InAbiNamespace) || - (nameToWrite == TypedefNameType.CCW && authoredType && !w.InAbiImplNamespace) || - (nameToWrite == TypedefNameType.CCW && !authoredType && (w.InAbiNamespace || w.InAbiImplNamespace))) - { - w.Write("global::"); - if (nameToWrite is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) - { - w.Write("ABI."); - } - else if (authoredType && nameToWrite == TypedefNameType.CCW) - { - w.Write("ABI.Impl."); - } - w.Write(typeNamespace); - w.Write("."); - } - - if (nameToWrite == TypedefNameType.StaticAbiClass) - { - w.WriteCode(typeName); - w.Write("Methods"); - } - else if (nameToWrite == TypedefNameType.EventSource) - { - w.WriteCode(typeName); - w.Write("EventSource"); - } - else - { - w.WriteCode(typeName); - } - } - - /// Mirrors C++ write_type_params: writes <T1, T2> for generic types. - public static void WriteTypeParams(TypeWriter w, TypeDefinition type) - { - if (type.GenericParameters.Count == 0) { return; } - w.Write("<"); - for (int i = 0; i < type.GenericParameters.Count; i++) - { - if (i > 0) { w.Write(", "); } - // For now, emit "T0", "T1" style placeholders - full generic args support requires the writer's stack. - string? gpName = type.GenericParameters[i].Name?.Value; - w.Write(gpName ?? $"T{i}"); - } - w.Write(">"); - } - - /// Mirrors C++ write_type_name: writes the typedef name + generic params. - public static void WriteTypeName(TypeWriter w, TypeSemantics semantics, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) - { - switch (semantics) - { - case TypeSemantics.Fundamental f: - WriteFundamentalType(w, f.Type); - break; - case TypeSemantics.Object_: - w.Write("object"); - break; - case TypeSemantics.Guid_: - w.Write("Guid"); - break; - case TypeSemantics.Type_: - w.Write("Type"); - break; - case TypeSemantics.Definition d: - WriteTypedefName(w, d.Type, nameType, forceWriteNamespace); - WriteTypeParams(w, d.Type); - break; - case TypeSemantics.GenericInstance gi: - WriteTypedefName(w, gi.GenericType, nameType, forceWriteNamespace); - w.Write("<"); - for (int i = 0; i < gi.GenericArgs.Count; i++) - { - if (i > 0) { w.Write(", "); } - // Generic args ALWAYS use Projected, regardless of parent's nameType. - // Mirrors C++ write_type_params -> write_generic_type_name_base -> write_projection_type - // (which is hard-coded to typedef_name_type::Projected). - WriteTypeName(w, gi.GenericArgs[i], TypedefNameType.Projected, forceWriteNamespace); - } - w.Write(">"); - break; - case TypeSemantics.GenericInstanceRef gir: - // Emit the type reference's full name with global:: qualification, applying mapped-type - // remapping if applicable (e.g., Windows.Foundation.IReference`1 -> System.Nullable, - // Windows.Foundation.TypedEventHandler`2 -> System.EventHandler). - { - string ns = gir.GenericType.Namespace?.Value ?? string.Empty; - string name = gir.GenericType.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - // Handle EventSource for Windows.Foundation event handlers (TypedEventHandler -> - // EventHandlerEventSource in WindowsRuntime.InteropServices). - if (nameType == TypedefNameType.EventSource && ns == "System") - { - w.Write("global::WindowsRuntime.InteropServices."); - } - else if (!string.IsNullOrEmpty(ns)) - { - w.Write("global::"); - if (nameType is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) - { - w.Write("ABI."); - } - w.Write(ns); - w.Write("."); - } - w.WriteCode(name); - if (nameType == TypedefNameType.StaticAbiClass) { w.Write("Methods"); } - else if (nameType == TypedefNameType.EventSource) { w.Write("EventSource"); } - - w.Write("<"); - for (int i = 0; i < gir.GenericArgs.Count; i++) - { - if (i > 0) { w.Write(", "); } - // Generic args ALWAYS use Projected, regardless of parent's nameType. - // Mirrors C++ write_type_params -> write_generic_type_name_base -> write_projection_type. - WriteTypeName(w, gir.GenericArgs[i], TypedefNameType.Projected, forceWriteNamespace); - } - w.Write(">"); - } - break; - case TypeSemantics.Reference r: - { - string ns = r.Reference_.Namespace?.Value ?? string.Empty; - string name = r.Reference_.Name?.Value ?? string.Empty; - MappedType? mapped = MappedTypes.Get(ns, name); - if (mapped is not null) - { - ns = mapped.MappedNamespace; - name = mapped.MappedName; - } - bool needsNsPrefix = !string.IsNullOrEmpty(ns) && ( - forceWriteNamespace || - ns != w.CurrentNamespace || - (nameType == TypedefNameType.Projected && (w.InAbiNamespace || w.InAbiImplNamespace)) || - (nameType == TypedefNameType.ABI && !w.InAbiNamespace) || - (nameType == TypedefNameType.EventSource && !w.InAbiNamespace) || - (nameType == TypedefNameType.CCW && (w.InAbiNamespace || w.InAbiImplNamespace))); - if (needsNsPrefix) - { - w.Write("global::"); - if (nameType is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) - { - w.Write("ABI."); - } - w.Write(ns); - w.Write("."); - } - w.WriteCode(name); - if (nameType == TypedefNameType.StaticAbiClass) { w.Write("Methods"); } - else if (nameType == TypedefNameType.EventSource) { w.Write("EventSource"); } - } - break; - case TypeSemantics.GenericTypeIndex gti: - w.Write($"T{gti.Index}"); - break; - } - } - - /// Mirrors C++ write_projection_type: writes a projected type name (.NET-style). - public static void WriteProjectionType(TypeWriter w, TypeSemantics semantics) - { - WriteTypeName(w, semantics, TypedefNameType.Projected, false); - } - - /// - /// Writes the event handler type for an EventDefinition. Handles all the cases: - /// TypeDefinition, TypeReference, TypeSpecification (generic instances like EventHandler<T>), - /// and any other ITypeDefOrRef. - /// - public static void WriteEventType(TypeWriter w, EventDefinition evt) - { - WriteEventType(w, evt, null); - } - - /// - /// Same as but applies the supplied - /// generic context for substitution (e.g., T0/T1 -> concrete type arguments - /// when emitting members for an instantiated parent generic interface). - /// - public static void WriteEventType(TypeWriter w, EventDefinition evt, AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature? currentInstance) - { - if (evt.EventType is null) - { - w.Write("global::Windows.Foundation.EventHandler"); - return; - } - AsmResolver.DotNet.Signatures.TypeSignature sig = evt.EventType.ToTypeSignature(false); - if (currentInstance is not null) - { - sig = sig.InstantiateGenericTypes(new AsmResolver.DotNet.Signatures.GenericContext(currentInstance, null)); - } - // Special case for Microsoft.UI.Xaml.Input.ICommand.CanExecuteChanged: the WinRT event - // handler is EventHandler but C# expects non-generic EventHandler. Mirrors C++: - // if (event.Name() == "CanExecuteChanged" && event_type == "global::System.EventHandler") - // check parent_type_name == ICommand and override event_type - if (evt.Name?.Value == "CanExecuteChanged" - && evt.DeclaringType is { } declaringType - && (declaringType.FullName == "Microsoft.UI.Xaml.Input.ICommand" - || declaringType.FullName == "Windows.UI.Xaml.Input.ICommand")) - { - // Verify the event type matches EventHandler before applying override. - if (sig is AsmResolver.DotNet.Signatures.GenericInstanceTypeSignature gi - && gi.GenericType.Namespace?.Value == "Windows.Foundation" - && gi.GenericType.Name?.Value == "EventHandler`1" - && gi.TypeArguments.Count == 1 - && gi.TypeArguments[0] is AsmResolver.DotNet.Signatures.CorLibTypeSignature corlib - && corlib.ElementType == AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object) - { - w.Write("global::System.EventHandler"); - return; - } - } - // Mirrors C++ write_event: typedef_name_type::Projected, forceWriteNamespace=false. - // The outer EventHandler still gets 'global::System.' from being in a different namespace, - // but type args in the same namespace stay unqualified. - WriteTypeName(w, TypeSemanticsFactory.Get(sig), TypedefNameType.Projected, false); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.cs b/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.cs deleted file mode 100644 index 88b5861724..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.cs +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using AsmResolver.DotNet; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Mirrors the C++ code_writers.h. Emits projected and ABI types. -/// -/// **STATUS**: This is a partial 1:1 port. The C++ code_writers.h file is ~11K lines containing -/// 295 functions. This C# port is a work-in-progress. The current implementation produces minimal -/// stub output for each type category to validate the architecture end-to-end. -/// -/// -internal static partial class CodeWriters -{ - /// - /// Dispatches type emission based on the type category. - /// - public static void WriteType(TypeWriter w, TypeDefinition type, TypeCategory category, Settings settings, MetadataCache cache) - { - switch (category) - { - case TypeCategory.Class: - if (TypeCategorization.IsAttributeType(type)) - { - WriteAttribute(w, type); - } - else - { - WriteClass(w, type); - } - break; - case TypeCategory.Delegate: - WriteDelegate(w, type); - break; - case TypeCategory.Enum: - WriteEnum(w, type); - break; - case TypeCategory.Interface: - WriteInterface(w, type); - break; - case TypeCategory.Struct: - if (TypeCategorization.IsApiContractType(type)) - { - WriteContract(w, type); - } - else - { - WriteStruct(w, type); - } - break; - } - } - - /// - /// Dispatches ABI emission based on the type category. - /// - public static void WriteAbiType(TypeWriter w, TypeDefinition type, TypeCategory category, Settings settings) - { - switch (category) - { - case TypeCategory.Class: - WriteAbiClass(w, type); - break; - case TypeCategory.Delegate: - WriteAbiDelegate(w, type); - WriteTempDelegateEventSourceSubclass(w, type); - break; - case TypeCategory.Enum: - WriteAbiEnum(w, type); - break; - case TypeCategory.Interface: - WriteAbiInterface(w, type); - break; - case TypeCategory.Struct: - WriteAbiStruct(w, type); - break; - } - } - - // ABI emission methods are implemented in CodeWriters.Abi.cs - - /// - /// Mirrors C++ write_enum. Emits an enum projection. - /// - public static void WriteEnum(TypeWriter w, TypeDefinition type) - { - if (w.Settings.Component) - { - return; - } - - bool isFlags = TypeCategorization.IsFlagsEnum(type); - string enumUnderlyingType = isFlags ? "uint" : "int"; - string accessibility = w.Settings.Internal ? "internal" : "public"; - string typeName = type.Name?.Value ?? string.Empty; - - if (isFlags) - { - w.Write("\n[FlagsAttribute]\n"); - } - else - { - w.Write("\n"); - } - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteValueTypeWinRTClassNameAttribute(w, type); - WriteTypeCustomAttributes(w, type, true); - WriteComWrapperMarshallerAttribute(w, type); - WriteWinRTReferenceTypeAttribute(w, type); - - w.Write(accessibility); - w.Write(" enum "); - w.Write(typeName); - w.Write(" : "); - w.Write(enumUnderlyingType); - w.Write("\n{\n"); - - foreach (FieldDefinition field in type.Fields) - { - if (field.Constant is null) - { - continue; - } - string fieldName = field.Name?.Value ?? string.Empty; - string constantValue = FormatConstant(field.Constant); - - // Mirror C++ code_writers.h:10106 write_platform_attribute(field.CustomAttribute()): - // emits per-enum-field [SupportedOSPlatform] when the field has a [ContractVersion]. - WritePlatformAttribute(w, field); - w.Write(fieldName); - w.Write(" = unchecked(("); - w.Write(enumUnderlyingType); - w.Write(")"); - w.Write(constantValue); - w.Write("),\n"); - } - w.Write("}\n\n"); - } - - /// - /// Formats a metadata Constant value as a C# literal. - /// Mirrors C++ write_constant: I4/U4 are formatted as hex (e.g. 0x1) to match - /// the truth output. Other types fall back to decimal. - /// - private static string FormatConstant(AsmResolver.DotNet.Constant constant) - { - // The Constant.Value contains raw bytes representing the value - AsmResolver.PE.DotNet.Metadata.Tables.ElementType type = constant.Type; - byte[] data = constant.Value?.Data ?? System.Array.Empty(); - return type switch - { - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1 => ((sbyte)data[0]).ToString(System.Globalization.CultureInfo.InvariantCulture), - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1 => data[0].ToString(System.Globalization.CultureInfo.InvariantCulture), - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2 => System.BitConverter.ToInt16(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2 => System.BitConverter.ToUInt16(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), - // I4/U4 use printf "%#0x" semantics: 0 -> "0", non-zero -> "0x" (alternate hex form omits prefix when value is zero). - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4 => FormatHexAlternate((uint)System.BitConverter.ToInt32(data, 0)), - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4 => FormatHexAlternate(System.BitConverter.ToUInt32(data, 0)), - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8 => System.BitConverter.ToInt64(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), - AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8 => System.BitConverter.ToUInt64(data, 0).ToString(System.Globalization.CultureInfo.InvariantCulture), - _ => "0" - }; - } - - private static string FormatHexAlternate(uint v) - { - // C++ printf "%#0x": for 0, outputs "0"; for non-zero, outputs "0x" with no padding. - if (v == 0) { return "0"; } - return "0x" + v.ToString("x", System.Globalization.CultureInfo.InvariantCulture); - } - - /// - /// Mirrors C++ write_struct. Emits a struct projection. - /// - public static void WriteStruct(TypeWriter w, TypeDefinition type) - { - if (w.Settings.Component) { return; } - - // Collect field info - System.Collections.Generic.List<(string TypeStr, string Name, string ParamName, bool IsInterface)> fields = new(); - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - TypeSemantics semantics = TypeSemanticsFactory.Get(field.Signature.FieldType); - string fieldType = w.WriteTemp("%", new System.Action(_ => WriteProjectionType(w, semantics))); - string fieldName = field.Name?.Value ?? string.Empty; - string paramName = ToCamelCase(fieldName); - bool isInterface = false; - if (semantics is TypeSemantics.Definition d) - { - isInterface = TypeCategorization.GetCategory(d.Type) == TypeCategory.Interface; - } - else if (semantics is TypeSemantics.GenericInstance gi) - { - isInterface = TypeCategorization.GetCategory(gi.GenericType) == TypeCategory.Interface; - } - fields.Add((fieldType, fieldName, paramName, isInterface)); - } - - string projectionName = type.Name?.Value ?? string.Empty; - bool hasAddition = AdditionTypes.HasAdditionToType(type.Namespace?.Value ?? string.Empty, projectionName); - - // Header attributes - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteValueTypeWinRTClassNameAttribute(w, type); - WriteTypeCustomAttributes(w, type, true); - WriteComWrapperMarshallerAttribute(w, type); - WriteWinRTReferenceTypeAttribute(w, type); - w.Write("public"); - if (hasAddition) { w.Write(" partial"); } - w.Write(" struct "); - w.Write(projectionName); - w.Write(": IEquatable<"); - w.Write(projectionName); - w.Write(">\n{\n"); - - // ctor - w.Write("public "); - w.Write(projectionName); - w.Write("("); - for (int i = 0; i < fields.Count; i++) - { - if (i > 0) { w.Write(", "); } - w.Write(fields[i].TypeStr); - w.Write(" "); - Helpers.WriteEscapedIdentifier(w, fields[i].ParamName); - } - w.Write(")\n{\n"); - foreach (var f in fields) - { - // When the param name matches the field name (i.e. ToCamelCase couldn't change casing), - // qualify with this. to disambiguate. - if (f.Name == f.ParamName) - { - w.Write("this."); - w.Write(f.Name); - w.Write(" = "); - Helpers.WriteEscapedIdentifier(w, f.ParamName); - w.Write("; "); - } - else - { - w.Write(f.Name); - w.Write(" = "); - Helpers.WriteEscapedIdentifier(w, f.ParamName); - w.Write("; "); - } - } - w.Write("\n}\n"); - - // properties - foreach (var f in fields) - { - w.Write("public "); - w.Write(f.TypeStr); - w.Write(" "); - w.Write(f.Name); - w.Write("\n{\nreadonly get; set;\n}\n"); - } - - // == - w.Write("public static bool operator ==("); - w.Write(projectionName); - w.Write(" x, "); - w.Write(projectionName); - w.Write(" y) => "); - if (fields.Count == 0) - { - w.Write("true"); - } - else - { - for (int i = 0; i < fields.Count; i++) - { - if (i > 0) { w.Write(" && "); } - w.Write("x."); - w.Write(fields[i].Name); - w.Write(" == y."); - w.Write(fields[i].Name); - } - } - w.Write(";\n"); - - // != - w.Write("public static bool operator !=("); - w.Write(projectionName); - w.Write(" x, "); - w.Write(projectionName); - w.Write(" y) => !(x == y);\n"); - - // equals - w.Write("public bool Equals("); - w.Write(projectionName); - w.Write(" other) => this == other;\n"); - - w.Write("public override bool Equals(object obj) => obj is "); - w.Write(projectionName); - w.Write(" that && this == that;\n"); - - // hashcode - w.Write("public override int GetHashCode() => "); - if (fields.Count == 0) - { - w.Write("0"); - } - else - { - for (int i = 0; i < fields.Count; i++) - { - if (i > 0) { w.Write(" ^ "); } - w.Write(fields[i].Name); - w.Write(".GetHashCode()"); - } - } - w.Write(";\n"); - w.Write("}\n\n"); - } - - /// - /// Mirrors C++ write_contract. Emits a static class for an API contract. - /// - public static void WriteContract(TypeWriter w, TypeDefinition type) - { - if (w.Settings.Component) { return; } - - string typeName = type.Name?.Value ?? string.Empty; - WriteTypeCustomAttributes(w, type, false); - w.Write(Helpers.InternalAccessibility(w.Settings)); - w.Write(" enum "); - w.Write(typeName); - w.Write("\n{\n}\n"); - } - - /// - /// Mirrors C++ write_delegate. Emits a delegate projection. - /// - public static void WriteDelegate(TypeWriter w, TypeDefinition type) - { - if (w.Settings.Component) { return; } - - MethodDefinition? invoke = Helpers.GetDelegateInvoke(type); - if (invoke is null) { return; } - MethodSig sig = new(invoke); - - w.Write("\n"); - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteTypeCustomAttributes(w, type, false); - WriteComWrapperMarshallerAttribute(w, type); - if (!w.Settings.ReferenceProjection) - { - // GUID attribute - w.Write("[Guid(\""); - WriteGuid(w, type, false); - w.Write("\")]\n"); - } - w.Write(Helpers.InternalAccessibility(w.Settings)); - w.Write(" delegate "); - WriteProjectionReturnType(w, sig); - w.Write(" "); - WriteTypedefName(w, type, TypedefNameType.Projected, false); - WriteTypeParams(w, type); - w.Write("("); - WriteParameterList(w, sig); - w.Write(");\n"); - } - - /// - /// Mirrors C++ write_attribute. Emits an attribute projection. - /// - public static void WriteAttribute(TypeWriter w, TypeDefinition type) - { - string typeName = type.Name?.Value ?? string.Empty; - - WriteWinRTMetadataAttribute(w, type, _cacheRef!); - WriteTypeCustomAttributes(w, type, true); - w.Write(Helpers.InternalAccessibility(w.Settings)); - w.Write(" sealed class "); - w.Write(typeName); - w.Write(": Attribute\n{\n"); - - // Constructors - foreach (MethodDefinition method in type.Methods) - { - if (method.Name?.Value != ".ctor") { continue; } - MethodSig sig = new(method); - w.Write("public "); - w.Write(typeName); - w.Write("("); - WriteParameterList(w, sig); - w.Write("){}\n"); - } - // Fields - foreach (FieldDefinition field in type.Fields) - { - if (field.IsStatic || field.Signature is null) { continue; } - w.Write("public "); - WriteProjectionType(w, TypeSemanticsFactory.Get(field.Signature.FieldType)); - w.Write(" "); - w.Write(field.Name?.Value ?? string.Empty); - w.Write(";\n"); - } - w.Write("}\n"); - } - - private static MetadataCache? _cacheRef; - - /// Sets the cache reference used by writers that need source-file paths. - public static void SetMetadataCache(MetadataCache cache) - { - _cacheRef = cache; - } - - /// Gets the metadata cache previously set via . - internal static MetadataCache? GetMetadataCache() => _cacheRef; - - /// Mirrors C++ to_camel_case. - public static string ToCamelCase(string name) - { - if (string.IsNullOrEmpty(name)) { return name; } - char c = name[0]; - if (c >= 'A' && c <= 'Z') - { - return char.ToLowerInvariant(c) + name.Substring(1); - } - return name; - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/TextWriter.cs b/src/WinRT.Projection.Generator.Writer/Writers/TextWriter.cs deleted file mode 100644 index 6907d9dee2..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/TextWriter.cs +++ /dev/null @@ -1,358 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Mirrors the C++ indented_writer_base in text_writer.h. -/// -/// Supports the C++ format placeholders: -/// -/// %: Insert any value (calls ). -/// @: Insert a code identifier (strips backticks, escapes invalid chars). -/// ^: Escape next character (usually %, @, or ^). -/// -/// Indentation is automatically managed: { increases indent by 4 spaces, } decreases. -/// -/// -internal class TextWriter -{ - private const int TabWidth = 4; - - private enum WriterState - { - None, - OpenParen, - OpenParenNewline, - } - - protected readonly List _first = new(16 * 1024); - private readonly List _second = new(); - - private WriterState _state = WriterState.None; - private readonly List _scopes = new() { 0 }; - private int _indent; - private bool _enableIndent = true; - - /// Writes a literal string verbatim (with indentation handling). - public void Write(ReadOnlySpan value) - { - for (int i = 0; i < value.Length; i++) - { - WriteChar(value[i]); - } - } - - /// Writes a literal string verbatim (with indentation handling). - public void Write(string value) - { - Write(value.AsSpan()); - } - - /// Writes a single character (with indentation handling). - public void Write(char value) - { - WriteChar(value); - } - - /// Writes an integer value. - public void Write(int value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); - - /// Writes an unsigned integer value. - public void Write(uint value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); - - /// Writes a long value. - public void Write(long value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); - - /// Writes an unsigned long value. - public void Write(ulong value) => Write(value.ToString(System.Globalization.CultureInfo.InvariantCulture)); - - /// Calls a writer callback. - public void Write(Action callback) - { - callback(this); - } - - /// Writes a value boxed as object. - public virtual void WriteValue(object? value) - { - switch (value) - { - case null: - break; - case string s: - Write(s); - break; - case char c: - Write(c); - break; - case int i: - Write(i); - break; - case uint ui: - Write(ui); - break; - case long l: - Write(l); - break; - case ulong ul: - Write(ul); - break; - case Action a: - a(this); - break; - default: - Write(value.ToString() ?? string.Empty); - break; - } - } - - /// Writes a code identifier (default: same as WriteValue, but specific writers may override). - public virtual void WriteCode(object? value) - { - if (value is string s) - { - WriteCode(s); - } - else - { - WriteValue(value); - } - } - - /// Writes a code identifier, stripping anything from a backtick onwards (matches C++ writer.write_code). - public virtual void WriteCode(string value) - { - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - if (c == '`') - { - return; - } - WriteChar(c); - } - } - - /// Writes a format string, with C++-style %/@/^ placeholders. - public void Write(string format, params object?[] args) - { - WriteSegment(format.AsSpan(), args, 0); - } - - /// Writes formatted string into temporary buffer and returns it (matches C++ write_temp). - public string WriteTemp(string format, params object?[] args) - { - bool restoreIndent = _enableIndent; - _enableIndent = false; - int sizeBefore = _first.Count; - - WriteSegment(format.AsSpan(), args, 0); - - string result = new string(CollectionsMarshalSpan(_first, sizeBefore, _first.Count - sizeBefore)); - _first.RemoveRange(sizeBefore, _first.Count - sizeBefore); - _enableIndent = restoreIndent; - return result; - } - - private static char[] CollectionsMarshalSpan(List list, int start, int length) - { - char[] arr = new char[length]; - for (int i = 0; i < length; i++) - { - arr[i] = list[start + i]; - } - return arr; - } - - /// Internal recursive segment writer. - private void WriteSegment(ReadOnlySpan value, object?[] args, int argIndex) - { - while (!value.IsEmpty) - { - int offset = -1; - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - if (c == '^' || c == '%' || c == '@') - { - offset = i; - break; - } - } - - if (offset < 0) - { - Write(value); - return; - } - - // Write everything up to the placeholder - if (offset > 0) - { - Write(value.Slice(0, offset)); - } - - char placeholder = value[offset]; - if (placeholder == '^') - { - Debug.Assert(offset + 1 < value.Length, "Escape ^ must be followed by another character"); - Write(value[offset + 1]); - value = value.Slice(offset + 2); - } - else if (placeholder == '%') - { - Debug.Assert(argIndex < args.Length, "Format string references more args than provided"); - WriteValue(args[argIndex++]); - value = value.Slice(offset + 1); - } - else // '@' - { - Debug.Assert(argIndex < args.Length, "Format string references more args than provided"); - WriteCode(args[argIndex++]); - value = value.Slice(offset + 1); - } - } - } - - /// Writes a single character with indentation handling. - private void WriteChar(char c) - { - // Normalize line endings: skip CR characters (we use LF only). - if (c == '\r') { return; } - - if (_enableIndent) - { - UpdateState(c); - if (_first.Count > 0 && _first[^1] == '\n' && c != '\n') - { - WriteIndent(); - } - } - _first.Add(c); - } - - private void WriteIndent() - { - for (int i = 0; i < _indent; i++) - { - _first.Add(' '); - } - } - - private void UpdateState(char c) - { - if (_state == WriterState.OpenParenNewline && c != ' ' && c != '\t') - { - _scopes[^1] = TabWidth; - _indent += TabWidth; - } - - switch (c) - { - case '{': - _state = WriterState.OpenParen; - _scopes.Add(0); - break; - case '}': - _state = WriterState.None; - _indent -= _scopes[^1]; - _scopes.RemoveAt(_scopes.Count - 1); - break; - case '\n': - _state = _state == WriterState.OpenParen ? WriterState.OpenParenNewline : WriterState.None; - break; - default: - _state = WriterState.None; - break; - } - } - - /// Returns the last character written (or '\0'). - public char Back() - { - return _first.Count == 0 ? '\0' : _first[^1]; - } - - /// Swaps the primary and secondary buffers. - public void Swap() - { - // Use a temp list since we can't swap List contents directly - char[] tmpArr = new char[_first.Count]; - _first.CopyTo(tmpArr); - _first.Clear(); - _first.AddRange(_second); - _second.Clear(); - _second.AddRange(tmpArr); - } - - /// Flushes both buffers to a string and clears them. - public string FlushToString() - { - StringBuilder sb = new(_first.Count + _second.Count); - for (int i = 0; i < _first.Count; i++) { sb.Append(_first[i]); } - for (int i = 0; i < _second.Count; i++) { sb.Append(_second[i]); } - _first.Clear(); - _second.Clear(); - return sb.ToString(); - } - - /// Flushes both buffers to a file and clears them; only writes if file content differs. - public void FlushToFile(string path) - { - // Build the full content - char[] arr = new char[_first.Count + _second.Count]; - _first.CopyTo(arr); - _second.CopyTo(arr, _first.Count); - string content = new string(arr); - - if (FileEqual(path, content)) - { - _first.Clear(); - _second.Clear(); - return; - } - - File.WriteAllText(path, content); - _first.Clear(); - _second.Clear(); - } - - private static bool FileEqual(string path, string content) - { - if (!File.Exists(path)) return false; - try - { - string existing = File.ReadAllText(path); - return existing == content; - } - catch - { - return false; - } - } - - /// Flushes to console out (matches C++ flush_to_console). - public void FlushToConsole() - { - for (int i = 0; i < _first.Count; i++) { Console.Write(_first[i]); } - for (int i = 0; i < _second.Count; i++) { Console.Write(_second[i]); } - _first.Clear(); - _second.Clear(); - } - - /// Flushes to console error (matches C++ flush_to_console_error). - public void FlushToConsoleError() - { - for (int i = 0; i < _first.Count; i++) { Console.Error.Write(_first[i]); } - for (int i = 0; i < _second.Count; i++) { Console.Error.Write(_second[i]); } - _first.Clear(); - _second.Clear(); - } -} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/TypeWriter.cs b/src/WinRT.Projection.Generator.Writer/Writers/TypeWriter.cs deleted file mode 100644 index 71af645454..0000000000 --- a/src/WinRT.Projection.Generator.Writer/Writers/TypeWriter.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace WindowsRuntime.ProjectionGenerator.Writer; - -/// -/// Mirrors the C++ writer class in type_writers.h. -/// Adds namespace context, generic parameter stack, and projection-specific begin/end helpers -/// on top of . -/// -internal sealed class TypeWriter : TextWriter -{ - public string CurrentNamespace { get; } - public Settings Settings { get; } - public bool InAbiNamespace { get; private set; } - public bool InAbiImplNamespace { get; private set; } - - /// - /// Mirrors C++ writer::_check_platform/_platform: when active inside a class scope, - /// platform-attribute computation suppresses platforms <= the previously seen platform. - /// - public bool CheckPlatform { get; set; } - public string Platform { get; set; } = string.Empty; - - /// Stack of generic argument lists currently in scope. - public List GenericArgsStack { get; } = new(); - - public TypeWriter(Settings settings, string currentNamespace) - { - Settings = settings; - CurrentNamespace = currentNamespace; - } - - public void WriteFileHeader() - { - Write("//------------------------------------------------------------------------------\n"); - Write("// \n"); - Write("// This file was generated by cswinrt.exe version "); - Write(CodeWriters.GetVersionString()); - Write("\n"); - Write("//\n"); - Write("// Changes to this file may cause incorrect behavior and will be lost if\n"); - Write("// the code is regenerated.\n"); - Write("// \n"); - Write("//------------------------------------------------------------------------------\n"); - Write( -@" -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Windows.Foundation; -using WindowsRuntime; -using WindowsRuntime.InteropServices; -using WindowsRuntime.InteropServices.Marshalling; -using static System.Runtime.InteropServices.ComWrappers; - -#pragma warning disable CS0169 // ""The field '...' is never used"" -#pragma warning disable CS0649 // ""Field '...' is never assigned to"" -#pragma warning disable CA2207, CA1063, CA1033, CA1001, CA2213 -#pragma warning disable CSWINRT3001 // ""Type or member '...' is a private implementation detail"" -#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type - -"); - } - - public void WriteBeginProjectedNamespace() - { - InAbiImplNamespace = Settings.Component; - string nsPrefix = Settings.Component ? "ABI.Impl." : string.Empty; - Write("\nnamespace "); - Write(nsPrefix); - Write(CurrentNamespace); - Write("\n{\n"); - } - - public void WriteEndProjectedNamespace() - { - Write("}\n"); - InAbiImplNamespace = false; - } - - public void WriteBeginAbiNamespace() - { - if (!Settings.NetstandardCompat) - { - Write("\n#pragma warning disable CA1416"); - } - Write("\nnamespace ABI."); - Write(CurrentNamespace); - Write("\n{\n"); - InAbiNamespace = true; - } - - public void WriteEndAbiNamespace() - { - Write("}\n"); - if (!Settings.NetstandardCompat) - { - Write("#pragma warning restore CA1416\n"); - } - InAbiNamespace = false; - } -} diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs index f794ad5781..1b45570912 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs @@ -8,7 +8,8 @@ using AsmResolver; using AsmResolver.DotNet; using WindowsRuntime.ProjectionGenerator.Errors; -using WindowsRuntime.ProjectionGenerator.Writer; +using WindowsRuntime.ProjectionWriter; +using WindowsRuntime.ProjectionWriter.Helpers; #pragma warning disable IDE0270 @@ -47,14 +48,13 @@ private static ProjectionGeneratorProcessingState ProcessReferences(ProjectionGe BuildWriterOptions( args, out string outputFolder, - out string rspFile, out HashSet projectionReferenceAssemblies, out bool hasTypesToProject, out ProjectionWriterOptions writerOptions); string[] referencesWithoutProjections = [.. args.ReferenceAssemblyPaths.Where(r => !projectionReferenceAssemblies.Contains(r))]; - return new ProjectionGeneratorProcessingState(outputFolder, rspFile, referencesWithoutProjections, writerOptions, hasTypesToProject); + return new ProjectionGeneratorProcessingState(outputFolder, referencesWithoutProjections, writerOptions, hasTypesToProject); } /// @@ -68,7 +68,7 @@ private static void GenerateSources(ProjectionGeneratorProcessingState processin // to be replaced/extended without needing to re-publish a separate executable. try { - ProjectionWriter.Run(processingState.WriterOptions); + global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run(processingState.WriterOptions); } catch (Exception e) when (!e.IsWellKnown) { @@ -77,20 +77,16 @@ private static void GenerateSources(ProjectionGeneratorProcessingState processin } /// - /// Builds the from the supplied arguments and reference assemblies, - /// also writing out a debug-only response file with the same options encoded in the historical - /// cswinrt.exe CLI format. + /// Builds the from the supplied arguments and reference assemblies. /// /// The arguments for this invocation. /// The folder where sources will be generated. - /// The path to the response file (kept as a debug artifact). /// The projection reference assemblies which were used. /// Whether any types were found to include in the projection. /// The resulting writer options. private static void BuildWriterOptions( ProjectionGeneratorArgs args, out string outputFolder, - out string rspFile, out HashSet projectionReferenceAssemblies, out bool hasTypesToProject, out ProjectionWriterOptions writerOptions) @@ -98,7 +94,6 @@ private static void BuildWriterOptions( args.Token.ThrowIfCancellationRequested(); outputFolder = GetTempFolder(); - rspFile = Path.Combine(outputFolder, "ProjectionGenerator.rsp"); projectionReferenceAssemblies = []; hasTypesToProject = false; @@ -106,8 +101,6 @@ private static void BuildWriterOptions( List excludes = []; List winmdInputs = []; - using StreamWriter fileStream = new(rspFile); - // Filter out .winmd files from the resolver paths string[] resolverPaths = [.. args.ReferenceAssemblyPaths .Where(p => !p.EndsWith(".winmd", StringComparison.OrdinalIgnoreCase))]; @@ -161,7 +154,6 @@ private static void BuildWriterOptions( continue; } - fileStream.WriteLine($"-include {type.FullName}"); includes.Add(type.FullName); hasTypesToProject = true; } @@ -209,7 +201,7 @@ private static void BuildWriterOptions( if (isWindowsSdk) { // Write the filters for the Windows SDK projection mode. - WriteWindowsSdkFilters(fileStream, includes, excludes, args.WindowsUIXamlProjection); + WriteWindowsSdkFilters(includes, excludes, args.WindowsUIXamlProjection); hasTypesToProject = true; @@ -219,14 +211,12 @@ private static void BuildWriterOptions( { // In addition to projecting the individual types, make sure // the additions get included by including the namespace. - fileStream.WriteLine($"-include Microsoft.UI"); includes.Add("Microsoft.UI"); } } foreach (TypeDefinition exportedType in moduleDefinition.TopLevelTypes) { - fileStream.WriteLine($"-include {exportedType.FullName}"); includes.Add(exportedType.FullName); hasTypesToProject = true; } @@ -237,7 +227,9 @@ private static void BuildWriterOptions( // (e.g., pipeline builds that pass WinMDs directly), hardcode the includes. if (isWindowsSdkMode && projectionReferenceAssemblies.Count == 0) { - WriteWindowsSdkFilters(fileStream, includes, excludes, args.WindowsUIXamlProjection); + WriteWindowsSdkFilters(includes, excludes, args.WindowsUIXamlProjection); + + hasTypesToProject = true; } // If we're not in Windows SDK mode, we exclude the Windows namespace to avoid @@ -245,30 +237,19 @@ private static void BuildWriterOptions( // and thereby no includes / excludes passed to the writer. if (!isWindowsSdkMode) { - fileStream.WriteLine("-exclude Windows"); excludes.Add("Windows"); } - fileStream.WriteLine($"-target {args.TargetFramework}"); - fileStream.WriteLine($"-input {args.WindowsMetadata}"); - fileStream.WriteLine($"-output \"{outputFolder}\""); - - // Expand the windows metadata token (path | "local" | "sdk[+]" | version[+]) into actual - // .winmd file paths (or directories the writer will recursively scan). The C++ cswinrt.exe - // tool did this in cmd_reader.h via reader.files() — see WindowsMetadataExpander. + // Expand the windows metadata token (path | "local" | "sdk[+]" | version[+]) into + // actual .winmd file paths (or directories the writer will recursively scan). winmdInputs.AddRange(WindowsMetadataExpander.Expand(args.WindowsMetadata)); // When generating 'WinRT.Component.dll', enable component-specific code generation // (activation factories, exclusive-to interfaces, etc.). bool componentMode = args.AssemblyName == "WinRT.Component"; - if (componentMode) - { - fileStream.WriteLine("-component"); - } foreach (string winmdPath in args.WinMDPaths) { - fileStream.WriteLine($"-input \"{winmdPath}\""); winmdInputs.Add(winmdPath); } @@ -279,35 +260,25 @@ private static void BuildWriterOptions( Include = includes, Exclude = excludes, Component = componentMode, + MaxDegreesOfParallelism = args.MaxDegreesOfParallelism, CancellationToken = args.Token, }; } /// - /// Writes the include/exclude filter directives for the Windows SDK projection. - /// Emits to both the .rsp file (for debugging) and the in-memory include/exclude lists - /// passed to . + /// Adds the include/exclude filter directives for the Windows SDK projection. /// - /// The RSP file writer. /// The list of namespace prefixes to include. /// The list of namespace prefixes to exclude. /// /// When true, writes the Windows.UI.Xaml filter set. /// When false, writes the base Windows SDK filter set. /// - private static void WriteWindowsSdkFilters(StreamWriter writer, List includes, List excludes, bool xamlProjection) + private static void WriteWindowsSdkFilters(List includes, List excludes, bool xamlProjection) { - void Include(string ns) - { - writer.WriteLine($"-include {ns}"); - includes.Add(ns); - } + void Include(string ns) => includes.Add(ns); - void Exclude(string ns) - { - writer.WriteLine($"-exclude {ns}"); - excludes.Add(ns); - } + void Exclude(string ns) => excludes.Add(ns); if (xamlProjection) { diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs index 7f90150ca2..f878d9e8d7 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.Parsing.cs @@ -82,6 +82,7 @@ public static ProjectionGeneratorArgs ParseFromResponseFile(string path, Cancell AssemblyName = GetOptionalStringArgument(argsMap, nameof(AssemblyName), "WinRT.Projection"), WindowsSdkOnly = GetOptionalBoolArgument(argsMap, nameof(WindowsSdkOnly)), WindowsUIXamlProjection = GetOptionalBoolArgument(argsMap, nameof(WindowsUIXamlProjection)), + MaxDegreesOfParallelism = GetInt32Argument(argsMap, nameof(MaxDegreesOfParallelism)), Token = token }; } @@ -167,4 +168,23 @@ private static bool GetOptionalBoolArgument(Dictionary argsMap, return false; } + + /// + /// Parses an argument. + /// + /// The input map with raw arguments. + /// The target property name. + /// The resulting argument. + private static int GetInt32Argument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + if (int.TryParse(argumentValue, out int parsedValue)) + { + return parsedValue; + } + } + + throw WellKnownProjectionGeneratorExceptions.ResponseFileArgumentParsingError(propertyName); + } } diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs index 2b8e4e6f2b..a2d4c3f0e4 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorArgs.cs @@ -54,6 +54,10 @@ internal sealed partial class ProjectionGeneratorArgs [CommandLineArgumentName("--windows-ui-xaml-projection")] public bool WindowsUIXamlProjection { get; init; } + /// Gets the maximum number of parallel tasks to use for execution. + [CommandLineArgumentName("--max-degrees-of-parallelism")] + public required int MaxDegreesOfParallelism { get; init; } + /// Gets the token for the operation. public required CancellationToken Token { get; init; } } \ No newline at end of file diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs index f47a0d13bf..fd1ede400c 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs @@ -1,21 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using WindowsRuntime.ProjectionWriter; + namespace WindowsRuntime.ProjectionGenerator.Generation; /// /// State produced by the processing phase of . /// /// The path to the folder where sources will be generated. -/// The path to the response file (kept as a debug artifact). /// The reference assembly paths excluding projection assemblies. -/// The options to pass to . +/// The options to pass to . /// Whether any types were found to project. internal sealed class ProjectionGeneratorProcessingState( string sourcesFolder, - string rspFilePath, string[] referencesWithoutProjections, - Writer.ProjectionWriterOptions writerOptions, + ProjectionWriterOptions writerOptions, bool hasTypesToProject = true) { /// @@ -23,20 +23,15 @@ internal sealed class ProjectionGeneratorProcessingState( /// public string SourcesFolder { get; } = sourcesFolder; - /// - /// Gets the path to the generated response file (kept as a debug artifact for inspection). - /// - public string RspFilePath { get; } = rspFilePath; - /// /// Gets the reference assembly paths excluding projection assemblies. /// public string[] ReferencesWithoutProjections { get; } = referencesWithoutProjections; /// - /// Gets the options used to invoke . + /// Gets the options used to invoke . /// - public Writer.ProjectionWriterOptions WriterOptions { get; } = writerOptions; + public ProjectionWriterOptions WriterOptions { get; } = writerOptions; /// /// Gets whether any types were found to project. When false, the source generation diff --git a/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj b/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj index ee1d463a13..250a7d02ae 100644 --- a/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj +++ b/src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj @@ -1,4 +1,4 @@ - + Exe net10.0 @@ -54,7 +54,7 @@ - + diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index 5fc71a4eeb..a73c9e6843 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -6,7 +6,8 @@ using System.IO; using System.Threading; using ConsoleAppFramework; -using WindowsRuntime.ProjectionGenerator.Writer; +using WindowsRuntime.ProjectionWriter; +using WindowsRuntime.ProjectionWriter.Helpers; using WindowsRuntime.ReferenceProjectionGenerator.Errors; namespace WindowsRuntime.ReferenceProjectionGenerator.Generation; @@ -64,7 +65,7 @@ public static void Run([Argument] string responseFilePath, CancellationToken tok { ConsoleApp.Log($"Generating reference projection sources -> {options.OutputFolder}"); - ProjectionWriter.Run(options); + global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run(options); } catch (Exception e) when (!e.IsWellKnown) { diff --git a/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj b/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj index f83cff36dd..d2070c128e 100644 --- a/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj +++ b/src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj @@ -1,4 +1,4 @@ - + Exe net10.0 @@ -51,7 +51,7 @@ - + diff --git a/src/WinRT.Projection.Generator.Writer.TestRunner/Program.cs b/src/WinRT.Projection.Writer.TestRunner/Program.cs similarity index 82% rename from src/WinRT.Projection.Generator.Writer.TestRunner/Program.cs rename to src/WinRT.Projection.Writer.TestRunner/Program.cs index b8152be4ae..86dbcd2986 100644 --- a/src/WinRT.Projection.Generator.Writer.TestRunner/Program.cs +++ b/src/WinRT.Projection.Writer.TestRunner/Program.cs @@ -3,9 +3,9 @@ using System; using System.IO; -using WindowsRuntime.ProjectionGenerator.Writer; +using WindowsRuntime.ProjectionWriter; -namespace WindowsRuntime.ProjectionGenerator.Writer.TestRunner; +namespace WindowsRuntime.ProjectionWriter.TestRunner; internal static class Program { @@ -36,10 +36,79 @@ public static int Main(string[] args) { return RunCompareAuthoring(args[1]); } + if (args.Length >= 2 && args[0] == "rsp") + { + return RunRsp(args[1], refMode); + } return RunSimple(args); } + /// + /// Reads a `.rsp` file (matching the orchestrator's response file format) and invokes + /// with the parsed options. Used by the refactor + /// validation harness to drive the writer with input-aligned scenarios. + /// + private static int RunRsp(string rspPath, bool refMode) + { + if (!File.Exists(rspPath)) { Console.Error.WriteLine($"RSP not found: {rspPath}"); return 1; } + string text = File.ReadAllText(rspPath); + var inputs = new System.Collections.Generic.List(); + var include = new System.Collections.Generic.List(); + var exclude = new System.Collections.Generic.List(); + string? outputFolder = null; + bool component = false, internalMode = false; + int maxDegreesOfParallelism = -1; + var tokens = new System.Collections.Generic.List(); + foreach (string raw in text.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) + { + string line = raw.Trim(); + if (line.Length == 0 || line.StartsWith('#')) { continue; } + int sp = line.IndexOf(' '); + if (sp < 0) { tokens.Add(line); } + else { tokens.Add(line.Substring(0, sp)); tokens.Add(line.Substring(sp + 1).Trim()); } + } + for (int i = 0; i < tokens.Count; i++) + { + string a = tokens[i]; + string? next = i + 1 < tokens.Count ? tokens[i + 1] : null; + switch (a) + { + case "--input-paths": case "--input-path": case "--input": + if (next is not null) { inputs.AddRange(next.Split(',', StringSplitOptions.RemoveEmptyEntries)); i++; } break; + case "--output-directory": case "--output-folder": case "--output": + if (next is not null) { outputFolder = next; i++; } break; + case "--include-namespaces": case "--include": + if (next is not null) { include.AddRange(next.Split(',', StringSplitOptions.RemoveEmptyEntries)); i++; } break; + case "--exclude-namespaces": case "--exclude": + if (next is not null) { exclude.AddRange(next.Split(',', StringSplitOptions.RemoveEmptyEntries)); i++; } break; + case "--component": component = true; break; + case "--internal": internalMode = true; break; + case "--reference-projection": refMode = true; break; + case "--max-degrees-of-parallelism": case "--mdop": + if (next is not null && int.TryParse(next, out int mdop)) { maxDegreesOfParallelism = mdop; i++; } break; + case "--target-framework": if (next is not null) { i++; } break; + } + } + if (outputFolder is null) { Console.Error.WriteLine("Missing --output-directory"); return 1; } + if (Directory.Exists(outputFolder)) { Directory.Delete(outputFolder, true); } + _ = Directory.CreateDirectory(outputFolder); + try + { + ProjectionWriter.Run(new ProjectionWriterOptions + { + InputPaths = inputs, OutputFolder = outputFolder, + Include = include, Exclude = exclude, + Component = component, Internal = internalMode, + ReferenceProjection = refMode, Verbose = false, + MaxDegreesOfParallelism = maxDegreesOfParallelism, + }); + } + catch (Exception ex) { Console.Error.WriteLine($"ERROR: {ex.Message}"); Console.Error.WriteLine(ex.StackTrace); return 1; } + Console.WriteLine($"Generated {Directory.GetFiles(outputFolder, "*.cs", SearchOption.AllDirectories).Length} files"); + return 0; + } + 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"; diff --git a/src/WinRT.Projection.Writer.TestRunner/WinRT.Projection.Writer.TestRunner.csproj b/src/WinRT.Projection.Writer.TestRunner/WinRT.Projection.Writer.TestRunner.csproj new file mode 100644 index 0000000000..8adeee69f3 --- /dev/null +++ b/src/WinRT.Projection.Writer.TestRunner/WinRT.Projection.Writer.TestRunner.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + 14.0 + enable + WindowsRuntime.ProjectionWriter.TestRunner + + + + + + diff --git a/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs new file mode 100644 index 0000000000..5059b38a5b --- /dev/null +++ b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs @@ -0,0 +1,434 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using AsmResolver.DotNet; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Builders; + +/// +/// Top-level dispatchers and emission for projected enums, structs, contracts, delegates, +/// and attribute classes. +/// +internal static class ProjectionFileBuilder +{ + /// + /// Dispatches type emission based on the type category. + /// + public static void WriteType(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, TypeCategory category) + { + switch (category) + { + case TypeCategory.Class: + if (TypeCategorization.IsAttributeType(type)) + { + WriteAttribute(writer, context, type); + } + else + { + ClassFactory.WriteClass(writer, context, type); + } + + break; + case TypeCategory.Delegate: + WriteDelegate(writer, context, type); + break; + case TypeCategory.Enum: + WriteEnum(writer, context, type); + break; + case TypeCategory.Interface: + InterfaceFactory.WriteInterface(writer, context, type); + break; + case TypeCategory.Struct: + if (TypeCategorization.IsApiContractType(type)) + { + WriteContract(writer, context, type); + } + else + { + WriteStruct(writer, context, type); + } + + break; + default: + throw WellKnownProjectionWriterExceptions.UnknownTypeCategory(category); + } + } + + /// + /// Dispatches ABI emission based on the type category. + /// + public static void WriteAbiType(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, TypeCategory category) + { + switch (category) + { + case TypeCategory.Class: + AbiClassFactory.WriteAbiClass(writer, context, type); + break; + case TypeCategory.Delegate: + AbiDelegateFactory.WriteAbiDelegate(writer, context, type); + AbiDelegateFactory.WriteDelegateEventSourceSubclass(writer, context, type); + break; + case TypeCategory.Enum: + AbiEnumFactory.WriteAbiEnum(writer, context, type); + break; + case TypeCategory.Interface: + AbiInterfaceFactory.WriteAbiInterface(writer, context, type); + break; + case TypeCategory.Struct: + AbiStructFactory.WriteAbiStruct(writer, context, type); + break; + default: + throw WellKnownProjectionWriterExceptions.UnknownTypeCategory(category); + } + } + + /// + /// Writes a projected enum (with [Flags] when applicable). + /// + public static void WriteEnum(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.Component) + { + return; + } + + bool isFlags = TypeCategorization.IsFlagsEnum(type); + string enumUnderlyingType = isFlags ? "uint" : "int"; + string accessibility = context.Settings.Internal ? "internal" : "public"; + string typeName = type.Name?.Value ?? string.Empty; + + writer.WriteLine(); + + if (isFlags) + { + writer.WriteLine("[Flags]"); + } + + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + MetadataAttributeFactory.WriteValueTypeWinRTClassNameAttribute(writer, context, type); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, true); + MetadataAttributeFactory.WriteComWrapperMarshallerAttribute(writer, context, type); + MetadataAttributeFactory.WriteWinRTReferenceTypeAttribute(writer, context, type); + + writer.WriteLine($$""" + {{accessibility}} enum {{typeName}} : {{enumUnderlyingType}} + """, isMultiline: true); + using (writer.WriteBlock()) + { + foreach (FieldDefinition field in type.Fields) + { + if (field.Constant is null) + { + continue; + } + + string fieldName = field.Name?.Value ?? string.Empty; + string constantValue = FormatConstant(field.Constant); + // Emits per-enum-field [SupportedOSPlatform] when the field has a [ContractVersion]. + CustomAttributeFactory.WritePlatformAttribute(writer, context, field); + writer.WriteLine($"{fieldName} = unchecked(({enumUnderlyingType}){constantValue}),"); + } + } + writer.WriteLine(); + } + + /// + /// Formats a metadata Constant value as a C# literal. + /// + internal static string FormatConstant(Constant constant) + { + // The Constant.Value contains raw bytes representing the value + ElementType type = constant.Type; + byte[] data = constant.Value?.Data ?? []; + return type switch + { + ElementType.I1 => ((sbyte)data[0]).ToString(CultureInfo.InvariantCulture), + ElementType.U1 => data[0].ToString(CultureInfo.InvariantCulture), + ElementType.I2 => BitConverter.ToInt16(data, 0).ToString(CultureInfo.InvariantCulture), + ElementType.U2 => BitConverter.ToUInt16(data, 0).ToString(CultureInfo.InvariantCulture), + // I4/U4 use printf "%#0x" semantics: 0 -> "0", non-zero -> "0x" + ElementType.I4 => FormatHexAlternate((uint)BitConverter.ToInt32(data, 0)), + ElementType.U4 => FormatHexAlternate(BitConverter.ToUInt32(data, 0)), + ElementType.I8 => BitConverter.ToInt64(data, 0).ToString(CultureInfo.InvariantCulture), + ElementType.U8 => BitConverter.ToUInt64(data, 0).ToString(CultureInfo.InvariantCulture), + _ => "0" + }; + } + + private static string FormatHexAlternate(uint v) + { + // Match printf "%#0x" semantics: for 0, output "0"; for non-zero, output "0x" with no padding. + if (v == 0) + { + return "0"; + } + + return "0x" + v.ToString("x", CultureInfo.InvariantCulture); + } + + /// + /// Writes a projected struct. + /// + public static void WriteStruct(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.Component) + { + return; + } + + // Collect field info + List<(string TypeStr, string Name, string ParamName, bool IsInterface)> fields = []; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + TypeSemantics semantics = TypeSemanticsFactory.Get(field.Signature.FieldType); + string fieldType = TypedefNameWriter.WriteProjectionType(context, semantics); + string fieldName = field.Name?.Value ?? string.Empty; + string paramName = IdentifierEscaping.ToCamelCase(fieldName); + bool isInterface = false; + + if (semantics is TypeSemantics.Definition d) + { + isInterface = TypeCategorization.GetCategory(d.Type) == TypeCategory.Interface; + } + else if (semantics is TypeSemantics.GenericInstance gi) + { + isInterface = TypeCategorization.GetCategory(gi.GenericType) == TypeCategory.Interface; + } + + fields.Add((fieldType, fieldName, paramName, isInterface)); + } + + string projectionName = type.Name?.Value ?? string.Empty; + bool hasAddition = AdditionTypes.HasAdditionToType(type.Namespace?.Value ?? string.Empty, projectionName); + + // Header attributes + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + MetadataAttributeFactory.WriteValueTypeWinRTClassNameAttribute(writer, context, type); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, true); + MetadataAttributeFactory.WriteComWrapperMarshallerAttribute(writer, context, type); + MetadataAttributeFactory.WriteWinRTReferenceTypeAttribute(writer, context, type); + writer.Write("public"); + + if (hasAddition) + { + writer.Write(" partial"); + } + + writer.WriteLine($" struct {projectionName} : IEquatable<{projectionName}>"); + using (writer.WriteBlock()) + { + writer.Write($"public {projectionName}("); + for (int i = 0; i < fields.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + writer.Write($"{fields[i].TypeStr} "); + IdentifierEscaping.WriteEscapedIdentifier(writer, fields[i].ParamName); + } + + writer.WriteLine(")"); + using (writer.WriteBlock()) + { + foreach ((string _, string name, string paramName, bool _) in fields) + { + // When the param name matches the field name (i.e. ToCamelCase couldn't change casing), + // qualify with this. to disambiguate. + if (name == paramName) + { + writer.Write($"this.{name} = "); + IdentifierEscaping.WriteEscapedIdentifier(writer, paramName); + writer.Write("; "); + } + else + { + writer.Write($"{name} = "); + IdentifierEscaping.WriteEscapedIdentifier(writer, paramName); + writer.Write("; "); + } + } + + writer.WriteLine(); + } + + // properties + foreach ((string typeStr, string name, string _, bool _) in fields) + { + writer.WriteLine($"public {typeStr} {name}"); + using (writer.WriteBlock()) + { + writer.WriteLine("readonly get; set;"); + } + } + + // == + writer.Write($"public static bool operator ==({projectionName} x, {projectionName} y) => "); + + if (fields.Count == 0) + { + writer.Write("true"); + } + else + { + for (int i = 0; i < fields.Count; i++) + { + if (i > 0) + { + writer.Write(" && "); + } + + writer.Write($"x.{fields[i].Name} == y.{fields[i].Name}"); + } + } + + writer.WriteLine(";"); + writer.WriteLine($"public static bool operator !=({projectionName} x, {projectionName} y) => !(x == y);"); + writer.WriteLine($"public bool Equals({projectionName} other) => this == other;"); + writer.WriteLine($"public override bool Equals(object obj) => obj is {projectionName} that && this == that;"); + writer.Write("public override int GetHashCode() => "); + + if (fields.Count == 0) + { + writer.Write("0"); + } + else + { + for (int i = 0; i < fields.Count; i++) + { + if (i > 0) + { + writer.Write(" ^ "); + } + + writer.Write($"{fields[i].Name}.GetHashCode()"); + } + } + + writer.WriteLine(";"); + } + + writer.WriteLine(); + } + + /// + /// Writes a projected API contract (an empty enum stand-in). + /// + public static void WriteContract(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.Component) + { + return; + } + + string typeName = type.Name?.Value ?? string.Empty; + + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, false); + writer.WriteLine($$""" + {{context.Settings.InternalAccessibility}} enum {{typeName}} + { + } + """, isMultiline: true); + } + + /// + /// Writes a projected delegate. + /// + public static void WriteDelegate(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.Component) + { + return; + } + + MethodDefinition? invoke = type.GetDelegateInvoke(); + + if (invoke is null) + { + return; + } + + MethodSignatureInfo sig = new(invoke); + + writer.WriteLine(); + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, false); + MetadataAttributeFactory.WriteComWrapperMarshallerAttribute(writer, context, type); + + if (!context.Settings.ReferenceProjection) + { + // GUID attribute + writer.Write("[Guid(\""); + IidExpressionGenerator.WriteGuid(writer, type, false); + writer.WriteLine("\")]"); + } + + writer.Write($"{context.Settings.InternalAccessibility} delegate "); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write(" "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write("("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.WriteLine(");"); + } + + /// + /// Writes a projected attribute class. + /// + public static void WriteAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string typeName = type.Name?.Value ?? string.Empty; + + writer.WriteLine(); + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, true); + writer.WriteLine($"{context.Settings.InternalAccessibility} sealed class {typeName} : Attribute"); + using (writer.WriteBlock()) + { + // Constructors + foreach (MethodDefinition method in type.Methods) + { + if (method.Name?.Value != ".ctor") + { + continue; + } + + MethodSignatureInfo sig = new(method); + writer.Write($"public {typeName}("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.WriteLine("){}"); + } + + // Fields + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + writer.Write("public "); + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(field.Signature.FieldType)); + writer.WriteLine($" {field.Name?.Value ?? string.Empty};"); + } + } + } +} diff --git a/src/WinRT.Projection.Writer/Errors/UnhandledProjectionWriterException.cs b/src/WinRT.Projection.Writer/Errors/UnhandledProjectionWriterException.cs new file mode 100644 index 0000000000..be6b57ed03 --- /dev/null +++ b/src/WinRT.Projection.Writer/Errors/UnhandledProjectionWriterException.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.ProjectionWriter.Errors; + +/// +/// An unhandled exception for the projection writer. +/// +internal sealed class UnhandledProjectionWriterException : Exception +{ + /// + /// The phase that failed. + /// + private readonly string _phase; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The phase that failed. + /// The inner exception. + public UnhandledProjectionWriterException(string phase, Exception exception) + : base(null, exception) + { + _phase = phase; + } + + /// + public override string ToString() + { + return + $"""error {WellKnownProjectionWriterExceptions.ErrorPrefix}9999: The CsWinRT projection writer failed with an unhandled exception """ + + $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the '{_phase}' phase. This might be due to an invalid """ + + $"""configuration in the current project, but the writer should still correctly identify that and fail gracefully. Please open an """ + + $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; + } +} diff --git a/src/WinRT.Projection.Writer/Errors/WellKnownProjectionWriterException.cs b/src/WinRT.Projection.Writer/Errors/WellKnownProjectionWriterException.cs new file mode 100644 index 0000000000..ca7ddada9c --- /dev/null +++ b/src/WinRT.Projection.Writer/Errors/WellKnownProjectionWriterException.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using System.Text; + +namespace WindowsRuntime.ProjectionWriter.Errors; + +/// +/// A well-known exception for the projection writer. +/// +internal sealed class WellKnownProjectionWriterException : Exception +{ + /// + /// The outer exception to include in the output, if available. + /// + private WellKnownProjectionWriterException? _outerException; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The id of the exception. + /// The exception message. + /// The inner exception, if any. + public WellKnownProjectionWriterException(string id, string message, Exception? innerException) + : base(message, innerException) + { + Id = id; + } + + /// + /// Gets the id of the exception. + /// + public string Id { get; } + + /// + public override string ToString() + { + // This method is only called when logging an exception message in case the + // tool fails. In that case, performance doesn't matter, so we can just use + // a normal 'StringBuilder' for convenience, no need to micro-optimize things. + StringBuilder builder = new(); + + // Always append the current exception info first + _ = builder.Append($"""error {Id}: {Message}"""); + + // If we have an inner (not well-known) exception, append its info. + // We only do this if the inner exception is not the same as the + // parent exception. That can happen when re-throwing exceptions. + // In that case, we only show the parent exception info below. + if (InnerException is Exception exception && exception != _outerException) + { + _ = builder.Append($""" Inner exception: '{exception.GetType().Name}': '{exception.Message}'."""); + } + + // If we have a parent well-known exception, append its info next + if (_outerException is not null) + { + _ = builder.Append($""" Outer exception: '{_outerException.Id}': '{_outerException.Message}'."""); + } + + return builder.ToString(); + } + + /// + /// Throws this exception, or attaches it as the parent of before re-throwing it. + /// + /// The exception to be re-thrown, if applicable. + [StackTraceHidden] + [DoesNotReturn] + public void ThrowOrAttach(Exception exception) + { + // For cancellation exceptions, we just always re-throw them as is. + if (exception is OperationCanceledException) + { + ExceptionDispatchInfo.Throw(exception); + } + + // For well-known exceptions, attach the current exception as the parent and re-throw. + // This allows the original exception to be the one that causes the failure, but with + // the additional context on the outer exception scope from the current exception also + // being included in the exception message. + if (exception is WellKnownProjectionWriterException originalException) + { + originalException._outerException = this; + + ExceptionDispatchInfo.Throw(exception); + } + + // In all other cases, just throw the current exception from this location. + // No need to capture the input exception -- callers will have already used it + // as the inner exception when constructing this instance. + throw this; + } +} diff --git a/src/WinRT.Projection.Writer/Errors/WellKnownProjectionWriterExceptions.cs b/src/WinRT.Projection.Writer/Errors/WellKnownProjectionWriterExceptions.cs new file mode 100644 index 0000000000..5f914f12f2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Errors/WellKnownProjectionWriterExceptions.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.ProjectionWriter.Errors; + +/// +/// Well-known exceptions produced by the projection writer. +/// +internal static class WellKnownProjectionWriterExceptions +{ + /// + /// The prefix for all error IDs produced by the writer. Shared with + /// WinRT.Projection.Generator; the writer uses the reserved 5000+ ID range so + /// writer and host-generator error IDs never collide. + /// + public const string ErrorPrefix = "CSWINRTPROJECTIONGEN"; + + /// + /// An internal invariant about a referenced type failed. + /// + public static WellKnownProjectionWriterException InternalInvariantFailed(string message) + { + return Exception(5001, message); + } + + /// + /// A metadata type referenced from an emission helper could not be resolved. + /// + public static WellKnownProjectionWriterException CannotResolveType(string typeName) + { + return Exception(5002, $"The type '{typeName}' could not be resolved against the metadata cache."); + } + + /// + /// A switch over the well-known TypeCategory enum encountered an unrecognized member. + /// + public static WellKnownProjectionWriterException UnknownTypeCategory(object category) + { + return Exception(5003, $"Unknown TypeCategory: {category}."); + } + + /// + /// A type signature passed to TypeSemanticsFactory could not be classified. + /// + public static WellKnownProjectionWriterException UnsupportedTypeSignature(string signature) + { + return Exception(5004, $"Unsupported signature: '{signature}'."); + } + + /// + /// A corlib element type passed to TypeSemanticsFactory.GetFundamental is not in the supported set. + /// + public static WellKnownProjectionWriterException UnsupportedCorLibElementType(object elementType) + { + return Exception(5005, $"Unsupported corlib element type: {elementType}."); + } + + /// + /// A fundamental type passed to IidExpressionGenerator is not in the supported set. + /// + public static WellKnownProjectionWriterException UnknownFundamentalType() + { + return Exception(5006, "Unknown fundamental type."); + } + + /// + /// A type referenced from IidExpressionGenerator is missing the expected [Guid] attribute or has malformed Guid fields. + /// + public static WellKnownProjectionWriterException MissingGuidAttribute(string typeName) + { + return Exception(5007, $"Type '{typeName}' is missing a usable [Guid] attribute or has malformed Guid fields."); + } + + /// + /// The orchestrator could not locate the Windows SDK install root in the registry. + /// + public static WellKnownProjectionWriterException WindowsSdkNotFound() + { + return Exception(5008, "Could not find the Windows SDK in the registry."); + } + + /// + /// The orchestrator could not read a Windows SDK platform XML file. + /// + public static WellKnownProjectionWriterException CannotReadWindowsSdkXml(string xmlPath) + { + return Exception(5009, $"Could not read the Windows SDK's XML at '{xmlPath}'."); + } + + /// + /// An emission helper detected a programming error (e.g. an unexpected null state). + /// + public static WellKnownProjectionWriterException UnreachableEmissionState(string message) + { + return Exception(5010, message); + } + + /// + /// An input metadata path passed to the loader does not point at an existing file or directory. + /// + public static WellKnownProjectionWriterException InvalidInputPath(string path) + { + return Exception(5011, $"The input metadata path '{path}' does not exist (must be a .winmd file or a directory containing one)."); + } + + /// + /// An input .winmd file is malformed or contains an unexpected number of modules. + /// + public static WellKnownProjectionWriterException MalformedWinmd(string path) + { + return Exception(5012, $"The input metadata file '{path}' is malformed: expected exactly one module per .winmd file."); + } + + /// + /// The parallel projection work-item loop terminated without enumerating every work item. + /// + public static WellKnownProjectionWriterException WorkItemLoopDidNotComplete() + { + return Exception(5013, "The parallel projection work-item loop did not complete; one or more work items were not dispatched."); + } + + /// + /// One of the projection work items dispatched by Run failed with an unexpected (non well-known) exception. + /// + public static WellKnownProjectionWriterException WorkItemLoopError(Exception exception) + { + return Exception(5014, "The parallel projection work-item loop reported one or more failures.", exception); + } + + /// + /// Emission of a single namespace's projection file failed. + /// + public static WellKnownProjectionWriterException NamespaceEmissionFailed(string namespaceName, Exception exception) + { + return Exception(5015, $"Failed to emit the projection file for namespace '{namespaceName}'.", exception); + } + + /// + /// Emission of the global GeneratedInterfaceIIDs.cs file failed. + /// + public static WellKnownProjectionWriterException GeneratedInterfaceIidsEmissionFailed(Exception exception) + { + return Exception(5016, "Failed to emit the global 'GeneratedInterfaceIIDs.cs' file.", exception); + } + + /// + /// Emission of the component-module activation-factory aggregator WinRT_Module.cs failed. + /// + public static WellKnownProjectionWriterException ComponentModuleEmissionFailed(Exception exception) + { + return Exception(5017, "Failed to emit the component-mode 'WinRT_Module.cs' activation-factory file.", exception); + } + + /// + /// The input projection options do not include any .winmd path. + /// + public static WellKnownProjectionWriterException MissingInputPaths() + { + return Exception(5018, "At least one input metadata path must be provided."); + } + + /// + /// The input projection options do not include an output folder. + /// + public static WellKnownProjectionWriterException MissingOutputFolder() + { + return Exception(5019, "An output folder must be provided."); + } + + /// + /// Settings.MakeReadOnly was called more than once on the same Settings instance. + /// + public static WellKnownProjectionWriterException SettingsAlreadyReadOnly() + { + return Exception(5020, "Settings have already been finalized via MakeReadOnly and cannot be finalized again."); + } + + /// + /// A derived Settings property was accessed before MakeReadOnly had been called. + /// + public static WellKnownProjectionWriterException SettingsNotReadOnly() + { + return Exception(5021, "Settings have not been finalized via MakeReadOnly; the derived state is not yet available."); + } + + private static WellKnownProjectionWriterException Exception(int id, string message, Exception? innerException = null) + { + return new WellKnownProjectionWriterException($"{ErrorPrefix}{id:0000}", message, innerException); + } +} diff --git a/src/WinRT.Projection.Writer/Extensions/EventDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/EventDefinitionExtensions.cs new file mode 100644 index 0000000000..45fe02a10c --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/EventDefinitionExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class EventDefinitionExtensions +{ + extension(EventDefinition evt) + { + /// + /// Returns the (add, remove) accessor pair of the event. + /// + /// A tuple of (Add, Remove) accessor methods, either of which may be . + public (MethodDefinition? Add, MethodDefinition? Remove) GetEventMethods() + => (evt.AddMethod, evt.RemoveMethod); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs new file mode 100644 index 0000000000..5f85f05698 --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class IHasCustomAttributeExtensions +{ + extension(IHasCustomAttribute member) + { + /// + /// Returns whether the member carries a custom attribute matching the given + /// and . + /// + /// The namespace of the attribute type. + /// The unqualified type name of the attribute. + /// if a matching custom attribute is found; otherwise . + public bool HasAttribute(string ns, string name) + { + foreach (CustomAttribute attr in member.CustomAttributes) + { + if (attr.Constructor?.DeclaringType is { } dt && + (dt.Namespace?.Value == ns) && + (dt.Name?.Value == name)) + { + return true; + } + } + return false; + } + + /// + /// Returns the matching custom attribute on the member, or + /// if none is found. + /// + /// The namespace of the attribute type. + /// The unqualified type name of the attribute. + /// The matching custom attribute, or if none is found. + public CustomAttribute? GetAttribute(string ns, string name) + { + foreach (CustomAttribute attr in member.CustomAttributes) + { + if (attr.Constructor?.DeclaringType is { } dt && + (dt.Namespace?.Value == ns) && + (dt.Name?.Value == name)) + { + return attr; + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/ITypeDefOrRefExtensions.cs b/src/WinRT.Projection.Writer/Extensions/ITypeDefOrRefExtensions.cs new file mode 100644 index 0000000000..2df65497b6 --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/ITypeDefOrRefExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class ITypeDefOrRefExtensions +{ + extension(ITypeDefOrRef type) + { + /// + /// Returns the namespace and name of the type as a tuple, with both fields + /// guaranteed to be non-: a missing namespace becomes + /// and a missing name becomes . + /// + /// A tuple of (namespace, name) with both fields non-. + public (string Namespace, string Name) Names() + { + return (type.Namespace?.Value ?? string.Empty, type.Name?.Value ?? string.Empty); + } + + /// + /// Attempts to resolve against , returning + /// when the type cannot be resolved (missing assembly, invalid reference, + /// missing type, etc.). This is the safe alternative to ITypeDescriptor.Resolve(RuntimeContext) + /// (which throws on failure) for best-effort cross-assembly resolution paths in the writer. + /// + /// The runtime context used to locate the type's assembly. + /// The resolved , or when the + /// reference cannot be resolved. + public TypeDefinition? TryResolve(RuntimeContext context) + { + return type.TryResolve(context, out TypeDefinition? definition) ? definition : null; + } + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/IndentedTextWriterExtensions.cs b/src/WinRT.Projection.Writer/Extensions/IndentedTextWriterExtensions.cs new file mode 100644 index 0000000000..6461b5f915 --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/IndentedTextWriterExtensions.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// General-purpose extension methods for that capture +/// repeated emission micro-patterns (separator lists, well-known prefixes, etc.). +/// +internal static class IndentedTextWriterExtensions +{ + extension(IndentedTextWriter writer) + { + /// + /// Writes each item in via , with + /// emitted between consecutive items. + /// + /// The item type. + /// The items to write. + /// The separator string emitted between consecutive items (e.g. ", "). + /// A callback that emits a single item. + public void WriteSeparated(IEnumerable items, string separator, Action writeItem) + { + bool first = true; + foreach (T item in items) + { + if (!first) + { + writer.Write(separator); + } + + writeItem(writer, item); + first = false; + } + } + + /// + /// Writes each item in via , with + /// ", " emitted between consecutive items. + /// + /// The item type. + /// The items to write. + /// A callback that emits a single item. + public void WriteCommaSeparated(IEnumerable items, Action writeItem) + { + writer.WriteSeparated(items, ", ", writeItem); + } + + /// + /// Writes the C# global namespace prefix () followed by + /// . Convenience wrapper for the common + /// writer.Write(GlobalPrefix); writer.Write(typeName); pattern. + /// + /// The fully-qualified type name to emit after the global:: prefix. + public void WriteGlobal(string typeName) + { + writer.Write($"{GlobalPrefix}{typeName}"); + } + + /// + /// Writes the fully-qualified ABI namespace prefix () followed + /// by . Convenience wrapper for the common + /// writer.Write(GlobalAbiPrefix); writer.Write(typeName); pattern. + /// + /// The dot-qualified type name to emit after the global::ABI. prefix. + public void WriteGlobalAbi(string typeName) + { + writer.Write($"{GlobalAbiPrefix}{typeName}"); + } + + /// + /// Writes the projection-wide accessibility modifier ("internal" when + /// or is set; otherwise + /// "public") for an emitted top-level type. + /// + /// The active projection settings. + public void WriteAccessibility(Settings settings) + { + writer.Write(settings.InternalAccessibility); + } + + /// + /// Writes a single C# attribute application of the form [name(args)] on its own + /// line. When is or empty, the parenthesis + /// list is omitted. + /// + /// The unqualified attribute name (without the trailing Attribute suffix). + /// The argument list to render between parentheses, or for no arguments. + public void WriteAttribute(string name, string? args = null) + { + if (string.IsNullOrEmpty(args)) + { + writer.WriteLine($"[{name}]"); + } + else + { + writer.WriteLine($"[{name}({args})]"); + } + } + + /// + /// Writes a fully-qualified reference to a well-known interface IID field + /// (global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_). + /// + /// The bare interface name (e.g. "IInspectable"). + public void WriteWellKnownIIDFieldRef(string interfaceName) + { + writer.Write($"global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_{interfaceName}"); + } + } +} + diff --git a/src/WinRT.Projection.Writer/Extensions/InterfaceImplementationExtensions.cs b/src/WinRT.Projection.Writer/Extensions/InterfaceImplementationExtensions.cs new file mode 100644 index 0000000000..c5c86d665a --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/InterfaceImplementationExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class InterfaceImplementationExtensions +{ + extension(InterfaceImplementation impl) + { + /// + /// Returns whether the implemented interface is the runtime class's [Default] interface + /// (i.e. the one whose vtable backs the class's IInspectable identity). + /// + /// if the interface is the default interface; otherwise . + public bool IsDefaultInterface() + => impl.HasAttribute(WindowsFoundationMetadata, DefaultAttribute); + + /// + /// Returns whether the implemented interface is marked [Overridable] (i.e. derived + /// classes are allowed to override its members). + /// + /// if the interface is overridable; otherwise . + public bool IsOverridable() + => impl.HasAttribute(WindowsFoundationMetadata, OverridableAttribute); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/MethodDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/MethodDefinitionExtensions.cs new file mode 100644 index 0000000000..2c093bd1ba --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/MethodDefinitionExtensions.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AsmResolver.DotNet; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class MethodDefinitionExtensions +{ + extension(MethodDefinition method) + { + /// + /// Returns whether the method is an instance constructor (i.e. its name is + /// .ctor and it is marked as runtime-special). + /// + /// if the method is an instance constructor; otherwise . + public bool IsConstructor() + => method.IsRuntimeSpecialName && method.Name == ".ctor"; + + /// + /// Returns whether the method is special (has either SpecialName or + /// RuntimeSpecialName set in its method attributes -- e.g. property accessors, + /// event accessors, constructors). + /// + /// if the method is marked special; otherwise . + public bool IsSpecial() + => method.IsSpecialName || method.IsRuntimeSpecialName; + + /// + /// Returns whether the method is the special remove_xxx event remover overload. + /// + /// if the method is an event remover; otherwise . + public bool IsRemoveOverload() + => method.IsSpecialName && (method.Name?.Value?.StartsWith("remove_", StringComparison.Ordinal) == true); + + /// + /// Returns whether the method carries the [NoExceptionAttribute] or is a + /// (event removers are implicitly no-throw). + /// + /// if the method is documented to never throw; otherwise . + public bool IsNoExcept() + => method.IsRemoveOverload() || method.HasAttribute(WindowsFoundationMetadata, NoExceptionAttribute); + } +} diff --git a/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExceptionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExceptionExtensions.cs new file mode 100644 index 0000000000..7f4c8c8ac2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExceptionExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.ProjectionWriter.Errors; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class ProjectionWriterExceptionExtensions +{ + extension(Exception exception) + { + /// + /// Gets a value indicating whether an exception is well known (and should therefore not be caught). + /// + public bool IsWellKnown => exception is OperationCanceledException or WellKnownProjectionWriterException; + } +} diff --git a/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs new file mode 100644 index 0000000000..851c19d54a --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// WinRT-projection-specific emission helpers for : file header, +/// projected/ABI namespace blocks, and pragma sections. +/// +internal static class ProjectionWriterExtensions +{ + extension(IndentedTextWriter writer) + { + /// + /// Writes the standard auto-generated file header (the cswinrt.exe banner + + /// canonical using imports + suppression pragmas) at the top of every emitted + /// .cs file. + /// + /// The active emit context (currently unused, but reserved for future per-namespace customization). + public void WriteFileHeader(ProjectionEmitContext context) + { + writer.WriteLine($$""" + //------------------------------------------------------------------------------ + // + // This file was generated by cswinrt.exe version {{MetadataAttributeFactory.GetVersionString()}} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.ComponentModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + using Windows.Foundation; + using WindowsRuntime; + using WindowsRuntime.InteropServices; + using WindowsRuntime.InteropServices.Marshalling; + using static System.Runtime.InteropServices.ComWrappers; + + #pragma warning disable CS0169 // "The field '...' is never used" + #pragma warning disable CS0649 // "Field '...' is never assigned to" + #pragma warning disable CA2207, CA1063, CA1033, CA1001, CA2213 + #pragma warning disable CSWINRT3001 // "Type or member '...' is a private implementation detail" + #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + + """, isMultiline: true); + } + + /// + /// Writes the opening namespace ... block for the projected namespace (with the + /// ABI.Impl. prefix when emitting a Windows Runtime component projection). + /// + /// The active emit context (provides the namespace + component flag). + public void WriteBeginProjectedNamespace(ProjectionEmitContext context) + { + string nsPrefix = context.Settings.Component ? "ABI.Impl." : string.Empty; + writer.WriteLine(); + writer.WriteLine($$""" + namespace {{nsPrefix}}{{context.CurrentNamespace}} + { + """, isMultiline: true); + writer.IncreaseIndent(); + if (context.Settings.Component) + { + context.InAbiImplNamespace = true; + } + } + + /// + /// Writes the closing } for the projected namespace and clears the active + /// flag. + /// + /// The active emit context. + public void WriteEndProjectedNamespace(ProjectionEmitContext context) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + context.InAbiImplNamespace = false; + } + + /// + /// Writes the opening namespace ABI.X block plus the #pragma warning disable CA1416 + /// suppression that wraps every ABI namespace. + /// + /// The active emit context (provides the namespace). + public void WriteBeginAbiNamespace(ProjectionEmitContext context) + { + writer.WriteLine(); + writer.WriteLine($$""" + #pragma warning disable CA1416 + namespace ABI.{{context.CurrentNamespace}} + { + """, isMultiline: true); + writer.IncreaseIndent(); + context.InAbiNamespace = true; + } + + /// + /// Writes the closing } for the ABI namespace plus the matching + /// #pragma warning restore CA1416 and clears the active + /// flag. + /// + /// The active emit context. + public void WriteEndAbiNamespace(ProjectionEmitContext context) + { + writer.DecreaseIndent(); + writer.WriteLine(""" + } + #pragma warning restore CA1416 + """, isMultiline: true); + context.InAbiNamespace = false; + } + } +} diff --git a/src/WinRT.Projection.Writer/Extensions/PropertyDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/PropertyDefinitionExtensions.cs new file mode 100644 index 0000000000..83daa0b3bc --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/PropertyDefinitionExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class PropertyDefinitionExtensions +{ + extension(PropertyDefinition property) + { + /// + /// Returns whether the property carries the [NoExceptionAttribute]. + /// + /// if the property is documented to never throw; otherwise . + public bool IsNoExcept() + => property.HasAttribute(WindowsFoundationMetadata, NoExceptionAttribute); + + /// + /// Returns the (getter, setter) accessor pair of the property. + /// + /// A tuple of (Getter, Setter) accessor methods, either of which may be . + public (MethodDefinition? Getter, MethodDefinition? Setter) GetPropertyMethods() + => (property.GetMethod, property.SetMethod); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/SettingsExtensions.cs b/src/WinRT.Projection.Writer/Extensions/SettingsExtensions.cs new file mode 100644 index 0000000000..e74460a0de --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/SettingsExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using WindowsRuntime.ProjectionWriter.Generation; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class SettingsExtensions +{ + extension(Settings settings) + { + /// + /// Gets the accessibility modifier ("public" or "internal") used for + /// generated types based on the and + /// flags. + /// + public string InternalAccessibility => settings.Internal || settings.Embedded ? "internal" : "public"; + } +} diff --git a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs new file mode 100644 index 0000000000..11b679339b --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for . +/// +internal static class TypeDefinitionExtensions +{ + extension(TypeDefinition type) + { + /// + /// Returns the [Default] interface of the type (the interface whose vtable backs the + /// type's IInspectable identity), or if the type does not declare one. + /// + /// The default interface, or . + public ITypeDefOrRef? GetDefaultInterface() + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.IsDefaultInterface() && impl.Interface is not null) + { + return impl.Interface; + } + } + return null; + } + + /// + /// Returns the Invoke method of a delegate type definition, or + /// if no such method exists. + /// + /// The delegate's Invoke method, or . + public MethodDefinition? GetDelegateInvoke() + { + foreach (MethodDefinition m in type.Methods) + { + if (m.IsSpecialName && m.Name == "Invoke") + { + return m; + } + } + return null; + } + + /// + /// Returns whether the type declares a parameterless instance constructor. + /// + /// if the type has a default constructor; otherwise . + public bool HasDefaultConstructor() + { + foreach (MethodDefinition m in type.Methods) + { + if (m.IsRuntimeSpecialName && m.Name == ".ctor" && m.Parameters.Count == 0) + { + return true; + } + } + return false; + } + + /// + /// Returns the second positional argument (a ) of + /// [Windows.Foundation.Metadata.ContractVersionAttribute] on the type, or + /// if the attribute is missing or the argument cannot be read. + /// + /// The contract version, or . + public int? GetContractVersion() + { + CustomAttribute? attr = type.GetAttribute(WindowsFoundationMetadata, ContractVersionAttribute); + + if (attr is null) + { + return null; + } + + 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; + } + + /// + /// Returns the first positional argument (a ) of + /// [Windows.Foundation.Metadata.VersionAttribute] on the type, or + /// if the attribute is missing or the argument cannot be read. + /// + /// The version, or . + public int? GetVersion() + { + CustomAttribute? attr = type.GetAttribute(WindowsFoundationMetadata, 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; + } + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs new file mode 100644 index 0000000000..c04801bec2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; +using static WindowsRuntime.ProjectionWriter.References.WellKnownTypeNames; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extension methods for : shape predicates and signature-tree +/// peeling helpers that do not require the metadata cache. +/// +/// +/// Predicates that need cross-module type resolution (e.g. IsBlittablePrimitive +/// with cross-module enum lookup, IsAnyStruct, IsComplexStruct) live in +/// and the ; +/// they are intentionally not included here. +/// +internal static class TypeSignatureExtensions +{ + extension(TypeSignature sig) + { + /// + /// Returns whether the signature is the corlib primitive. + /// + /// if the signature is System.String; otherwise . + public bool IsString() + { + return sig is CorLibTypeSignature corlib && corlib.ElementType == ElementType.String; + } + + /// + /// Returns whether the signature is the corlib primitive. + /// + /// if the signature is System.Object; otherwise . + public bool IsObject() + { + return sig is CorLibTypeSignature corlib && corlib.ElementType == ElementType.Object; + } + + /// + /// Returns whether the signature is (or a TypeRef/TypeSpec + /// that resolves to it, including the WinRT Windows.UI.Xaml.Interop.TypeName struct + /// that is mapped to it). + /// + /// if the signature is the projected System.Type; otherwise . + public bool IsSystemType() + { + if (sig is TypeDefOrRefSignature td && td.Type is { } t) + { + (string ns, string name) = t.Names(); + + if (ns == "System" && name == "Type") + { + return true; + } + + if (ns == WindowsUIXamlInterop && name == TypeName) + { + return true; + } + } + + return false; + } + + /// + /// Returns whether the signature is a WinRT IReference<T> or a + /// instantiation (both project to Nullable<T> in C#). + /// + /// if the signature is a Nullable-shaped generic instantiation; otherwise . + public bool IsNullableT() + { + if (sig is not GenericInstanceTypeSignature gi) + { + return false; + } + + string ns = gi.GenericType?.Namespace?.Value ?? string.Empty; + string name = gi.GenericType?.Name?.Value ?? string.Empty; + return (ns == WindowsFoundation && name == IReferenceGeneric) + || (ns == "System" && name == NullableGeneric); + } + + /// + /// Returns the single type argument of a generic instance type signature, or + /// if the signature is not a single-arg generic instance. Used to peel the inner T from + /// Nullable<T> / IReference<T>. + /// + /// The inner type argument, or . + public TypeSignature? GetNullableInnerType() + { + if (sig is GenericInstanceTypeSignature gi && gi.TypeArguments.Count == 1) + { + return gi.TypeArguments[0]; + } + + return null; + } + + /// + /// Returns whether the signature is a generic instantiation (i.e. requires + /// WinRT.Interop UnsafeAccessor-based marshalling). + /// + /// if the signature is a ; otherwise . + public bool IsGenericInstance() + { + return sig is GenericInstanceTypeSignature; + } + + /// + /// Returns whether the signature is the special System.Exception / + /// Windows.Foundation.HResult pair (which uses an HResult struct as its ABI form + /// and requires custom marshalling via ABI.System.ExceptionMarshaller). + /// + /// if the signature is the projected HResult/Exception; otherwise . + public bool IsHResultException() + { + if (sig is not TypeDefOrRefSignature td || td.Type is null) + { + return false; + } + + (string ns, string name) = td.Type.Names(); + return (ns == "System" && name == "Exception") + || (ns == WindowsFoundation && name == HResult); + } + } + + extension(TypeSignature? sig) + { + /// + /// Strips trailing and + /// wrappers from the signature, returning the underlying signature (or + /// if the input is ). + /// + /// The underlying signature with byref + custom-modifier wrappers stripped. + public TypeSignature? StripByRefAndCustomModifiers() + { + 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; + } + + /// + /// Returns whether the signature represents a by-reference type, peeling any + /// custom-modifier wrappers (e.g. modreq[InAttribute]) before checking. + /// + /// if the signature (after peeling custom modifiers) is a ; otherwise . + public bool IsByRefType() + { + TypeSignature? cur = sig; + while (cur is CustomModifierTypeSignature cm) + { + cur = cm.BaseType; + } + return cur is ByReferenceTypeSignature; + } + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Factories/AbiClassFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiClassFactory.cs new file mode 100644 index 0000000000..1cd9e3cad7 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiClassFactory.cs @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the full ABI surface for a projected runtime class type: +/// the marshaller stub, ComWrappers callback, and authoring-metadata wrapper. +/// +internal static class AbiClassFactory +{ + /// + /// Emits the full ABI surface for a projected runtime class: marshaller stub, ComWrappers callback, and authoring-metadata wrapper. + /// + public static void WriteAbiClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // Static classes don't get a *Marshaller (no instances). + if (TypeCategorization.IsStatic(type)) + { + return; + } + + writer.WriteLine("#nullable enable"); + + if (context.Settings.Component) + { + WriteComponentClassMarshaller(writer, context, type); + WriteAuthoringMetadataType(writer, context, type); + } + else + { + // Emit a ComWrappers marshaller class so the attribute reference resolves + WriteClassMarshallerStub(writer, context, type); + } + + writer.WriteLine("#nullable disable"); + } + + /// + /// Emits the simpler component-mode class marshaller. + /// + internal static void WriteComponentClassMarshaller(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string nameStripped = IdentifierEscaping.StripBackticks(type.Name?.Value ?? string.Empty); + string typeNs = type.Namespace?.Value ?? string.Empty; + string projectedType = $"global::{typeNs}.{nameStripped}"; + + ITypeDefOrRef? defaultIface = type.GetDefaultInterface(); + // 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. + GenericInstanceTypeSignature? defaultGenericInst = null; + + if (defaultIface is TypeSpecification spec + && spec.Signature is GenericInstanceTypeSignature gi) + { + defaultGenericInst = gi; + } + + string defaultIfaceIid; + + if (defaultGenericInst is not null) + { + // Call the accessor: '>(null)'. + string accessorName = ObjRefNameGenerator.BuildIidPropertyNameForGenericInterface(context, defaultGenericInst); + defaultIfaceIid = accessorName + "(null)"; + } + else + { + defaultIfaceIid = defaultIface is not null + ? ObjRefNameGenerator.WriteIidExpression(context, defaultIface) + : "default(global::System.Guid)"; + } + + writer.WriteLine(); + writer.WriteLine($$""" + public static unsafe class {{nameStripped}}Marshaller + { + public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged({{projectedType}} value) + { + """, isMultiline: true); + if (defaultGenericInst is not null) + { + // Emit the UnsafeAccessor declaration (uses 'object?' since component-mode + // marshallers run inside #nullable enable). + string accessorBlock = ObjRefNameGenerator.EmitUnsafeAccessorForIid(context, defaultGenericInst, isInNullableContext: true); + // Re-emit each line indented by 8 spaces. + string[] accessorLines = accessorBlock.TrimEnd('\n').Split('\n'); + foreach (string accessorLine in accessorLines) + { + writer.WriteLine($" {accessorLine}"); + } + } + + writer.WriteLine($$""" + return WindowsRuntimeInterfaceMarshaller<{{projectedType}}>.ConvertToUnmanaged(value, {{defaultIfaceIid}}); + } + + public static {{projectedType}}? ConvertToManaged(void* value) + { + return ({{projectedType}}?) WindowsRuntimeObjectMarshaller.ConvertToManaged(value); + } + } + """, isMultiline: true); + } + + /// + /// Emits the metadata wrapper type file static class <Name> {} with the conditional + /// set of attributes required for the type's category. + /// + internal static void WriteAuthoringMetadataType(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string nameStripped = IdentifierEscaping.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 is not (TypeCategory.Delegate or TypeCategory.Class)) + { + writer.WriteLine($"[WindowsRuntimeReferenceType(typeof({projectedType}?))]"); + } + + // [ABI..ComWrappersMarshaller] for non-struct, non-class types + // (delegates, enums, interfaces). + if (category is not (TypeCategory.Struct or TypeCategory.Class)) + { + writer.WriteLine($"[ABI.{typeNs}.{nameStripped}ComWrappersMarshaller]"); + } + + // [WindowsRuntimeClassName("Windows.Foundation.IReference`1<.>")] for non-class types. + if (category != TypeCategory.Class) + { + writer.WriteLine($"[WindowsRuntimeClassName(\"Windows.Foundation.IReference`1<{fullName}>\")]"); + } + + writer.WriteLine($$""" + [WindowsRuntimeMetadataTypeName("{{fullName}}")] + [WindowsRuntimeMappedType(typeof({{projectedType}}))] + file static class {{nameStripped}} {} + """, isMultiline: true); + } + + /// + /// Returns whether the ABI impl type for should be emitted in the current settings (component vs reference vs exclusive-to scope). + /// + public static bool EmitImplType(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.Component) + { + return true; + } + + if (TypeCategorization.IsExclusiveTo(type) && !context.Settings.PublicExclusiveTo) + { + // 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 = AbiTypeHelpers.GetExclusiveToType(context.Cache, type); + + if (exclusiveToType is null) + { + return true; + } + + bool hasOverridable = false; + foreach (InterfaceImplementation impl in exclusiveToType.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + TypeDefinition? ifaceTd = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, impl.Interface); + + if (ifaceTd == type && impl.IsOverridable()) + { + hasOverridable = true; + break; + } + } + return hasOverridable; + } + + 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) + /// and write_class_comwrappers_callback. + /// + internal static void WriteClassMarshallerStub(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.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 = type.GetDefaultInterface(); + string defaultIfaceIid = defaultIface is not null + ? ObjRefNameGenerator.WriteIidExpression(context, defaultIface) + : "default(global::System.Guid)"; + + // Determine the marshalingType expression from the class's [MarshalingBehaviorAttribute]. + // The same value is used for both the marshaller attribute and the callback. + string marshalingType = ConstructorFactory.GetMarshalingTypeName(type); + + bool isSealed = type.IsSealed; + + // For unsealed classes, the ConvertToUnmanaged path needs to know whether the default interface is + // exclusive-to. + TypeDefinition? defaultIfaceTd = defaultIface is null ? null : AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, defaultIface); + bool defaultIfaceIsExclusive = defaultIfaceTd is not null && TypeCategorization.IsExclusiveTo(defaultIfaceTd); + + // Public *Marshaller class + writer.WriteLine($$""" + public static unsafe class {{nameStripped}}Marshaller + { + public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged({{fullProjected}} value) + { + """, isMultiline: true); + if (isSealed) + { + // For projected sealed runtime classes, the RCW type is always unwrappable. + writer.WriteLine(""" + if (value is not null) + { + return WindowsRuntimeComWrappersMarshal.UnwrapObjectReferenceUnsafe(value).AsValue(); + } + """, isMultiline: true); + } + else if (!defaultIfaceIsExclusive && defaultIface is not null) + { + string defIfaceTypeName = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(defaultIface.ToTypeSignature(false)), TypedefNameType.Projected, false); + writer.WriteLine($$""" + if (value is IWindowsRuntimeInterface<{{defIfaceTypeName}}> windowsRuntimeInterface) + { + return windowsRuntimeInterface.GetInterface(); + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + if (value is not null) + { + return value.GetDefaultInterface(); + } + """, isMultiline: true); + } + writer.WriteLine($$""" + return default; + } + + public static {{fullProjected}}? ConvertToManaged(void* value) + { + return ({{fullProjected}}?){{(isSealed ? "WindowsRuntimeObjectMarshaller" : "WindowsRuntimeUnsealedObjectMarshaller")}}.ConvertToManaged<{{nameStripped}}ComWrappersCallback>(value); + } + } + + file sealed unsafe class {{nameStripped}}ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute + """, isMultiline: true); + using (writer.WriteBlock()) + { + AbiMethodBodyFactory.EmitUnsafeAccessorForDefaultIfaceIfGeneric(writer, context, defaultIface); + writer.WriteLine($$""" + public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) + { + WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReference( + externalComObject: value, + iid: {{defaultIfaceIid}}, + marshalingType: {{marshalingType}}, + wrapperFlags: out wrapperFlags); + + return new {{fullProjected}}(valueReference); + } + """, isMultiline: true); + } + + writer.WriteLine(); + + if (isSealed) + { + // file-scoped *ComWrappersCallback - implements IWindowsRuntimeObjectComWrappersCallback + writer.WriteLine($$""" + file sealed unsafe class {{nameStripped}}ComWrappersCallback : IWindowsRuntimeObjectComWrappersCallback + { + """, isMultiline: true); + AbiMethodBodyFactory.EmitUnsafeAccessorForDefaultIfaceIfGeneric(writer, context, defaultIface); + writer.WriteLine($$""" + public static object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) + { + WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe( + externalComObject: value, + iid: {{defaultIfaceIid}}, + marshalingType: {{marshalingType}}, + wrapperFlags: out wrapperFlags); + + return new {{fullProjected}}(valueReference); + } + } + """, isMultiline: true); + } + else + { + // file-scoped *ComWrappersCallback - implements IWindowsRuntimeUnsealedObjectComWrappersCallback + string nonProjectedRcn = $"{typeNs}.{nameStripped}"; + writer.WriteLine($$""" + file sealed unsafe class {{nameStripped}}ComWrappersCallback : IWindowsRuntimeUnsealedObjectComWrappersCallback + { + """, isMultiline: true); + AbiMethodBodyFactory.EmitUnsafeAccessorForDefaultIfaceIfGeneric(writer, context, defaultIface); + + // TryCreateObject (non-projected runtime class name match) + writer.WriteLine($$""" + public static unsafe bool TryCreateObject( + void* value, + ReadOnlySpan runtimeClassName, + [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? wrapperObject, + out CreatedWrapperFlags wrapperFlags) + { + if (runtimeClassName.SequenceEqual("{{nonProjectedRcn}}".AsSpan())) + { + WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe( + externalComObject: value, + iid: {{defaultIfaceIid}}, + marshalingType: {{marshalingType}}, + wrapperFlags: out wrapperFlags); + + wrapperObject = new {{fullProjected}}(valueReference); + return true; + } + + wrapperObject = null; + wrapperFlags = CreatedWrapperFlags.None; + return false; + } + + public static unsafe object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) + { + WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe( + externalComObject: value, + iid: {{defaultIfaceIid}}, + marshalingType: {{marshalingType}}, + wrapperFlags: out wrapperFlags); + + return new {{fullProjected}}(valueReference); + } + } + """, isMultiline: true); + } + } + +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiDelegateFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiDelegateFactory.cs new file mode 100644 index 0000000000..1d2f2b33a3 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiDelegateFactory.cs @@ -0,0 +1,466 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the full ABI surface for a projected delegate type: +/// the marshaller class, vtable, native delegate, ComWrappers callback, interface entries, +/// ComWrappers marshaller attribute, the impl class, and the IReference impl. +/// +internal static class AbiDelegateFactory +{ + /// + /// Emits the full ABI surface for a projected delegate type: marshaller class, vtable, native delegate, ComWrappers callback, interface entries, ComWrappers marshaller attribute, impl class, and IReference impl. + /// + public static void WriteAbiDelegate(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // 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(writer, context, type); + WriteDelegateVftbl(writer, context, type); + WriteNativeDelegate(writer, context, type); + WriteDelegateComWrappersCallback(writer, context, type); + WriteDelegateInterfaceEntriesImpl(writer, context, type); + WriteDelegateComWrappersMarshallerAttribute(writer, context, type); + WriteDelegateImpl(writer, context, type); + ReferenceImplFactory.WriteReferenceImpl(writer, context, type); + + // In component mode, the original code also emits the authoring metadata wrapper for delegates. + if (context.Settings.Component) + { + AbiClassFactory.WriteAuthoringMetadataType(writer, context, type); + } + } + + /// + /// Emits the <DelegateName>Impl static class providing the CCW vtable for a delegate. + /// + private static void WriteDelegateImpl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) + { + return; + } + + MethodDefinition? invoke = type.GetDelegateInvoke(); + + if (invoke is null) + { + return; + } + + MethodSignatureInfo sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + string iidExpr = ObjRefNameGenerator.WriteIidExpression(context, type); + + writer.WriteLine(); + writer.Write($$""" + internal static unsafe class {{nameStripped}}Impl + { + [FixedAddressValueType] + private static readonly {{nameStripped}}Vftbl Vftbl; + + static {{nameStripped}}Impl() + { + *(IUnknownVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IUnknownVftbl*)IUnknownImpl.Vtable; + Vftbl.Invoke = &Invoke; + } + + public static nint Vtable + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)Unsafe.AsPointer(in Vftbl); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int Invoke( + """, isMultiline: true); + AbiInterfaceFactory.WriteAbiParameterTypesPointer(writer, context, sig, includeParamNames: true); + writer.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 = TypedefNameWriter.WriteTypedefName(context, type, TypedefNameType.Projected, true); + + if (!projectedDelegateForBody.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + projectedDelegateForBody = GlobalPrefix + projectedDelegateForBody; + } + + AbiMethodBodyFactory.EmitDoAbiBodyIfSimple(writer, context, sig, projectedDelegateForBody, "Invoke"); + writer.WriteLine(); + writer.WriteLine($$""" + public static ref readonly Guid IID + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref {{iidExpr}}; + } + } + """, isMultiline: true); + } + + private static void WriteDelegateVftbl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) + { + return; + } + + MethodDefinition? invoke = type.GetDelegateInvoke(); + + if (invoke is null) + { + return; + } + + MethodSignatureInfo sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + writer.WriteLine(); + writer.Write($$""" + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct {{nameStripped}}Vftbl + { + public delegate* unmanaged[MemberFunction] QueryInterface; + public delegate* unmanaged[MemberFunction] AddRef; + public delegate* unmanaged[MemberFunction] Release; + public delegate* unmanaged[MemberFunction]< + """, isMultiline: true); + AbiInterfaceFactory.WriteAbiParameterTypesPointer(writer, context, sig); + writer.WriteLine(""" + , int> Invoke; + } + """, isMultiline: true); + } + + private static void WriteNativeDelegate(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) + { + return; + } + + MethodDefinition? invoke = type.GetDelegateInvoke(); + + if (invoke is null) + { + return; + } + + MethodSignatureInfo sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + writer.WriteLine(); + writer.Write($$""" + public static unsafe class {{nameStripped}}NativeDelegate + { + public static unsafe + """, isMultiline: true); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {nameStripped}Invoke(this WindowsRuntimeObjectReference thisReference"); + + if (sig.Parameters.Count > 0) + { + writer.Write(", "); + } + + MethodFactory.WriteParameterList(writer, context, sig); + writer.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. + AbiMethodBodyFactory.EmitAbiMethodBodyIfSimple(writer, context, sig, slot: 3, isNoExcept: invoke.IsNoExcept()); + + writer.WriteLine("}"); + } + + private static void WriteDelegateInterfaceEntriesImpl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (type.GenericParameters.Count > 0) + { + return; + } + + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + string iidExpr = ObjRefNameGenerator.WriteIidExpression(context, type); + string iidRefExpr = ObjRefNameGenerator.WriteIidReferenceExpression(type); + + writer.WriteLine(); + writer.WriteLine($$""" + file static class {{nameStripped}}InterfaceEntriesImpl + { + [FixedAddressValueType] + public static readonly DelegateReferenceInterfaceEntries Entries; + + static {{nameStripped}}InterfaceEntriesImpl() + { + Entries.Delegate.IID = {{iidExpr}}; + Entries.Delegate.Vtable = {{nameStripped}}Impl.Vtable; + Entries.DelegateReference.IID = {{iidRefExpr}}; + Entries.DelegateReference.Vtable = {{nameStripped}}ReferenceImpl.Vtable; + """, isMultiline: true); + writer.IncreaseIndent(); + writer.IncreaseIndent(); + WellKnownInterfaceEntriesEmitter.EmitDelegateReferenceWellKnownEntries(writer); + writer.DecreaseIndent(); + writer.DecreaseIndent(); + writer.WriteLine(""" + } + } + """, isMultiline: true); + } + + /// + /// Emits a per-delegate EventSource subclass that adapts the given non-generic + /// delegate type to the runtime's EventSource<TDelegate> abstraction. Generic + /// delegates (e.g. EventHandler<T>) are handled by the generic + /// EventHandlerEventSource<T> instead and are skipped here. + /// + /// The output writer. + /// The active emission context. + /// The delegate type to generate the EventSource subclass for. + public static void WriteDelegateEventSourceSubclass(IndentedTextWriter writer, ProjectionEmitContext context, 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 = type.GetDelegateInvoke(); + + if (invoke is null) + { + return; + } + + MethodSignatureInfo sig = new(invoke); + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + // Compute the projected type name (with global::) used as the generic argument. + string projectedName = TypedefNameWriter.WriteTypedefName(context, type, TypedefNameType.Projected, true); + + if (!projectedName.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + projectedName = GlobalPrefix + projectedName; + } + + writer.WriteLine(); + writer.Write($$""" + public sealed unsafe class {{nameStripped}}EventSource : EventSource<{{projectedName}}> + { + /// + public {{nameStripped}}EventSource(WindowsRuntimeObjectReference nativeObjectReference, int index) + : base(nativeObjectReference, index) + { + } + + /// + protected override WindowsRuntimeObjectReferenceValue ConvertToUnmanaged({{projectedName}} value) + { + return {{nameStripped}}Marshaller.ConvertToUnmanaged(value); + } + + /// + protected override EventSourceState<{{projectedName}}> CreateEventSourceState() + { + return new EventState(GetNativeObjectReferenceThisPtrUnsafe(), Index); + } + + private sealed class EventState : EventSourceState<{{projectedName}}> + { + /// + public EventState(void* thisPtr, int index) + : base(thisPtr, index) + { + } + + /// + protected override {{projectedName}} GetEventInvoke() + { + return ( + """, isMultiline: true); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + ParameterCategory pc = ParameterCategoryResolver.GetParamCategory(sig.Parameters[i]); + + if (pc == ParameterCategory.Ref) + { + writer.Write("in "); + } + else if (pc is ParameterCategory.Out or ParameterCategory.ReceiveArray) + { + writer.Write("out "); + } + + string raw = sig.Parameters[i].Parameter.Name ?? "p"; + writer.Write(CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw); + } + writer.Write(") => TargetDelegate.Invoke("); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + ParameterCategory pc = ParameterCategoryResolver.GetParamCategory(sig.Parameters[i]); + + if (pc == ParameterCategory.Ref) + { + writer.Write("in "); + } + else if (pc is ParameterCategory.Out or ParameterCategory.ReceiveArray) + { + writer.Write("out "); + } + + string raw = sig.Parameters[i].Parameter.Name ?? "p"; + writer.Write(CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw); + } + writer.WriteLine(""" + ); + } + } + } + """, isMultiline: true); + } + + /// + /// Writes a marshaller stub for a delegate. + /// + /// + /// Emits just the <Name>Marshaller class for a delegate. + /// + private static void WriteDelegateMarshallerOnly(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + string typeNs = type.Namespace?.Value ?? string.Empty; + string fullProjected = $"global::{typeNs}.{nameStripped}"; + string iidExpr = ObjRefNameGenerator.WriteIidExpression(context, type); + + writer.WriteLine(); + writer.WriteLine($$""" + public static unsafe class {{nameStripped}}Marshaller + { + public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged({{fullProjected}} value) + { + return WindowsRuntimeDelegateMarshaller.ConvertToUnmanaged(value, in {{iidExpr}}); + } + + #nullable enable + public static {{fullProjected}}? ConvertToManaged(void* value) + { + return ({{fullProjected}}?)WindowsRuntimeDelegateMarshaller.ConvertToManaged<{{nameStripped}}ComWrappersCallback>(value); + } + #nullable disable + } + """, isMultiline: true); + } + + /// + /// Emits the <Name>ComWrappersCallback file-scoped class for a delegate. + /// here at all — the higher-level dispatch in ProjectionGenerator filters out generic + /// types from ABI emission . Open generic delegates + /// can't compile this body anyway because the projected type would have unbound generic + /// parameters. + /// + private static void WriteDelegateComWrappersCallback(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + string typeNs = type.Namespace?.Value ?? string.Empty; + string fullProjected = $"global::{typeNs}.{nameStripped}"; + string iidExpr = ObjRefNameGenerator.WriteIidExpression(context, type); + + writer.WriteLine(); + writer.WriteLine($$""" + file abstract unsafe class {{nameStripped}}ComWrappersCallback : IWindowsRuntimeObjectComWrappersCallback + { + /// + public static object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) + { + WindowsRuntimeObjectReference valueReference = WindowsRuntimeComWrappersMarshal.CreateObjectReferenceUnsafe( + externalComObject: value, + iid: in {{iidExpr}}, + wrapperFlags: out wrapperFlags); + + return new {{fullProjected}}(valueReference.{{nameStripped}}Invoke); + } + } + """, isMultiline: true); + } + + /// + /// Emits the <Name>ComWrappersMarshallerAttribute class. + /// write_delegate_com_wrappers_marshaller_attribute_impl. Generic delegates are not + /// emitted here at all (filtered out in ProjectionGenerator). + /// + private static void WriteDelegateComWrappersMarshallerAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + string iidRefExpr = ObjRefNameGenerator.WriteIidReferenceExpression(type); + + writer.WriteLine(); + writer.WriteLine($$""" + internal sealed unsafe class {{nameStripped}}ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute + { + /// + public override void* GetOrCreateComInterfaceForObject(object value) + { + return WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.TrackerSupport); + } + + /// + public override ComInterfaceEntry* ComputeVtables(out int count) + { + count = sizeof(DelegateReferenceInterfaceEntries) / sizeof(ComInterfaceEntry); + + return (ComInterfaceEntry*)Unsafe.AsPointer(in {{nameStripped}}InterfaceEntriesImpl.Entries); + } + + /// + public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) + { + wrapperFlags = CreatedWrapperFlags.NonWrapping; + return WindowsRuntimeDelegateMarshaller.UnboxToManaged<{{nameStripped}}ComWrappersCallback>(value, in {{iidRefExpr}})!; + } + } + """, isMultiline: true); + } + +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiEnumFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiEnumFactory.cs new file mode 100644 index 0000000000..beeef97cbb --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiEnumFactory.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the ABI marshaller class and the IReference<T> impl for a projected enum type. +/// +internal static class AbiEnumFactory +{ + /// + /// Writes the ABI marshaller class and IReference impl for an enum type. + /// + /// The writer to emit to. + /// The active emit context. + /// The enum type definition. + public static void WriteAbiEnum(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + StructEnumMarshallerFactory.WriteStructEnumMarshallerClass(writer, context, type); + ReferenceImplFactory.WriteReferenceImpl(writer, context, type); + + // In component mode, also emit the authoring metadata wrapper for enums. + if (context.Settings.Component) + { + AbiClassFactory.WriteAuthoringMetadataType(writer, context, type); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs new file mode 100644 index 0000000000..dea45ff2a4 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs @@ -0,0 +1,632 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the full ABI surface for a projected interface type: +/// the marshaller stub, vtable, impl class, marshaller class, and ABI parameter-list helpers. +/// +internal static class AbiInterfaceFactory +{ + /// + /// Emits the full ABI surface for a projected interface type: marshaller stub, vtable, impl class, marshaller class, and ABI parameter-list helpers. + /// + public static void WriteAbiInterface(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // Generic interfaces are handled by interopgen + if (type.GenericParameters.Count > 0) + { + return; + } + + // Emit the per-interface marshaller stub. + WriteInterfaceMarshallerStub(writer, context, type); + + // For internal projections, just the static ABI methods class is enough. + if (TypeCategorization.IsProjectionInternal(type)) + { + return; + } + + WriteInterfaceVftbl(writer, context, type); + WriteInterfaceImpl(writer, context, type); + AbiInterfaceIDicFactory.WriteInterfaceIdicImpl(writer, context, type); + WriteInterfaceMarshaller(writer, context, type); + } + + /// + /// Writes the ABI parameter types for a vtable function pointer signature. + /// + public static void WriteAbiParameterTypesPointer(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig) + { + WriteAbiParameterTypesPointer(writer, context, 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(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig, bool includeParamNames) + { + // void* thisPtr, then each param's ABI type, then return type pointer + writer.Write("void*"); + + if (includeParamNames) + { + writer.Write(" thisPtr"); + } + + for (int i = 0; i < sig.Parameters.Count; i++) + { + writer.Write(", "); + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (p.Type is SzArrayTypeSignature) + { + // length pointer + value pointer. + if (includeParamNames) + { + writer.Write($"uint __{p.Parameter.Name ?? "param"}Size, void* "); + IdentifierEscaping.WriteEscapedIdentifier(writer, p.Parameter.Name ?? "param"); + } + else + { + writer.Write("uint, void*"); + } + } + else if (p.Type is ByReferenceTypeSignature br) + { + // Special case: 'out T[]' is a ReceiveArray ABI signature: (uint* size, T** data). + if (br.BaseType is SzArrayTypeSignature brSz && cat == ParameterCategory.ReceiveArray) + { + bool isRefElemBr = brSz.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(brSz.BaseType) || brSz.BaseType.IsObject() || brSz.BaseType.IsGenericInstance(); + + if (includeParamNames) + { + writer.Write($"uint* __{p.Parameter.Name ?? "param"}Size, "); + + if (isRefElemBr) + { + writer.Write("void*** "); + } + else + { + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(brSz.BaseType)); + writer.Write("** "); + } + + IdentifierEscaping.WriteEscapedIdentifier(writer, p.Parameter.Name ?? "param"); + } + else + { + writer.Write("uint*, "); + + if (isRefElemBr) + { + writer.Write("void***"); + } + else + { + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(brSz.BaseType)); + writer.Write("**"); + } + } + } + else + { + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(br.BaseType)); + writer.Write("*"); + + if (includeParamNames) + { + writer.Write(" "); + IdentifierEscaping.WriteEscapedIdentifier(writer, p.Parameter.Name ?? "param"); + } + } + } + else + { + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(p.Type)); + + if (cat is ParameterCategory.Out or ParameterCategory.Ref) + { + writer.Write("*"); + } + + if (includeParamNames) + { + writer.Write(" "); + IdentifierEscaping.WriteEscapedIdentifier(writer, p.Parameter.Name ?? "param"); + } + } + } + + // Return parameter + if (sig.ReturnType is not null) + { + writer.Write(", "); + string retName = AbiTypeHelpers.GetReturnParamName(sig); + string retSizeName = AbiTypeHelpers.GetReturnSizeParamName(sig); + // Special handling for SzArray return types: WinRT projects them as a (uint*, T**) pair. + if (sig.ReturnType is SzArrayTypeSignature retSz) + { + if (includeParamNames) + { + writer.Write($"uint* {retSizeName}, "); + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(retSz.BaseType)); + writer.Write($"** {retName}"); + } + else + { + writer.Write("uint*, "); + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(retSz.BaseType)); + writer.Write("**"); + } + } + else + { + AbiTypeWriter.WriteAbiType(writer, context, TypeSemanticsFactory.Get(sig.ReturnType)); + writer.Write("*"); + + if (includeParamNames) + { + writer.Write($" {retName}"); + } + } + } + } + + /// + /// Emits the per-interface vtable struct ({Name}Vftbl) with IUnknown/IInspectable function pointer fields followed by one field per interface method. + /// + public static void WriteInterfaceVftbl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (!AbiClassFactory.EmitImplType(writer, context, type)) + { + return; + } + + if (type.GenericParameters.Count > 0) + { + return; + } + + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + writer.WriteLine(); + writer.WriteLine($$""" + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct {{nameStripped}}Vftbl + """, isMultiline: true); + using (writer.WriteBlock()) + { + writer.WriteLine(""" + public delegate* unmanaged[MemberFunction] QueryInterface; + public delegate* unmanaged[MemberFunction] AddRef; + public delegate* unmanaged[MemberFunction] Release; + public delegate* unmanaged[MemberFunction] GetIids; + public delegate* unmanaged[MemberFunction] GetRuntimeClassName; + public delegate* unmanaged[MemberFunction] GetTrustLevel; + """, isMultiline: true); + + foreach (MethodDefinition method in type.Methods) + { + string vm = AbiTypeHelpers.GetVirtualMethodName(type, method); + MethodSignatureInfo sig = new(method); + writer.Write("public delegate* unmanaged[MemberFunction]<"); + WriteAbiParameterTypesPointer(writer, context, sig); + writer.WriteLine($", int> {vm};"); + } + } + } + + /// + /// Emits the ABI implementation for a runtime interface type (vtable struct, IUnknown/IInspectable entries, Methods class, and CCW Do_Abi handlers). + /// + public static void WriteInterfaceImpl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (!AbiClassFactory.EmitImplType(writer, context, type)) + { + return; + } + + if (type.GenericParameters.Count > 0) + { + return; + } + + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + writer.WriteLine(); + writer.WriteLine($"public static unsafe class {nameStripped}Impl"); + using IndentedTextWriter.Block __implBlock = writer.WriteBlock(); + writer.WriteLine($$""" + [FixedAddressValueType] + private static readonly {{nameStripped}}Vftbl Vftbl; + + static {{nameStripped}}Impl() + { + *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable; + """, isMultiline: true); + foreach (MethodDefinition method in type.Methods) + { + string vm = AbiTypeHelpers.GetVirtualMethodName(type, method); + writer.WriteLine($" Vftbl.{vm} = &Do_Abi_{vm};"); + } + writer.Write(""" + } + + public static ref readonly Guid IID + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref + """, isMultiline: true); + AbiTypeHelpers.WriteIidGuidReference(writer, context, type); + writer.WriteLine(""" + ; + } + + public static nint Vtable + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)Unsafe.AsPointer(in Vftbl); + } + """, isMultiline: true); + writer.WriteLine(); + + // Do_Abi_* implementations: emit real bodies for simple primitive cases, + // throw null! for everything else (deferred — needs full per-parameter marshalling). + // 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 (context.Settings.Component) + { + MetadataCache cache = context.Cache; + exclusiveToOwner = AbiTypeHelpers.GetExclusiveToType(cache, type); + + 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 = IdentifierEscaping.StripBackticks(exclusiveToOwner.Name?.Value ?? string.Empty); + ifaceFullName = string.IsNullOrEmpty(ownerNs) + ? GlobalPrefix + ownerNm + : GlobalPrefix + 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 = IdentifierEscaping.StripBackticks(type.Name?.Value ?? string.Empty); + ifaceFullName = string.IsNullOrEmpty(ifaceNs) + ? "global::ABI.Impl." + ifaceNm + : "global::ABI.Impl." + ifaceNs + "." + ifaceNm; + } + else + { + ifaceFullName = TypedefNameWriter.WriteTypedefName(context, type, TypedefNameType.Projected, true); + + if (!ifaceFullName.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + ifaceFullName = GlobalPrefix + 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. + Dictionary? eventMap = AbiTypeHelpers.BuildEventMethodMap(type); + + // Build sets of property accessors and event accessors so the first loop below can + // iterate "regular" methods (non-property, non-event) only. Do_Abi bodies are emitted in + // this order: methods first, then properties (setter before getter), then events. + HashSet propertyAccessors = []; + 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 = AbiTypeHelpers.GetVirtualMethodName(type, method); + MethodSignatureInfo 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. + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) + { + EventTableFactory.EmitEventTableField(writer, context, evt, ifaceFullName); + } + + writer.Write($$""" + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static unsafe int Do_Abi_{{vm}}( + """, isMultiline: true); + WriteAbiParameterTypesPointer(writer, context, sig, includeParamNames: true); + writer.Write(")"); + + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt2)) + { + if (evt2.AddMethod == method) + { + EventTableFactory.EmitDoAbiAddEvent(writer, context, evt2, sig, ifaceFullName); + } + else + { + EventTableFactory.EmitDoAbiRemoveEvent(writer, context, evt2, sig, ifaceFullName); + } + } + else + { + AbiMethodBodyFactory.EmitDoAbiBodyIfSimple(writer, context, 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); + } + } + } + + /// + /// Emits the per-interface marshaller class ({Name}Marshaller) with the boxing/unboxing helpers used by user code to marshal references across the ABI. + /// + public static void WriteInterfaceMarshaller(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (TypeCategorization.IsExclusiveTo(type)) + { + return; + } + + if (type.GenericParameters.Count > 0) + { + return; + } + + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + writer.WriteLine(); + writer.Write($$""" + #nullable enable + public static unsafe class {{nameStripped}}Marshaller + { + public static WindowsRuntimeObjectReferenceValue ConvertToUnmanaged( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(""" + value) + { + return WindowsRuntimeInterfaceMarshaller< + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(">.ConvertToUnmanaged(value, "); + AbiTypeHelpers.WriteIidGuidReference(writer, context, type); + writer.Write(""" + ); + } + + public static + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(""" + ? ConvertToManaged(void* value) + { + return ( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine(""" + ?) WindowsRuntimeObjectMarshaller.ConvertToManaged(value); + } + } + #nullable disable + """, isMultiline: true); + } + + /// + /// Writes a minimal interface 'Methods' static class with method body emission. + /// 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(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + // exclusive to a class (and not opted into PublicExclusiveTo) or if it's marked + // [ProjectionInternal]; public otherwise. + bool useInternal = (TypeCategorization.IsExclusiveTo(type) && !context.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 + if (ClassFactory.IsFastAbiOtherInterface(context.Cache, 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 original code also + // omits these because their owning class is not projected. + if (TypeCategorization.IsExclusiveTo(type)) + { + TypeDefinition? owningClass = AbiTypeHelpers.GetExclusiveToType(context.Cache, type); + + if (owningClass is not null && !context.Settings.Filter.Includes(owningClass)) + { + return; + } + } + + // are inlined in the RCW class, so we skip emitting them in the Methods type. + bool skipExclusiveEvents = false; + + if (TypeCategorization.IsExclusiveTo(type) && !context.Settings.PublicExclusiveTo) + { + TypeDefinition? classType = AbiTypeHelpers.GetExclusiveToType(context.Cache, type); + + if (classType is not null) + { + foreach (InterfaceImplementation impl in classType.Interfaces) + { + TypeDefinition? implDef = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, 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. + // For non-fast-abi interfaces, the segment list is just [(type, INSPECTABLE_METHOD_COUNT, skipExclusiveEvents)]. + const int InspectableMethodCount = 6; + List<(TypeDefinition Iface, int StartSlot, bool SkipEvents)> segments = []; + (TypeDefinition Class, TypeDefinition? Default, List Others)? fastAbi = ClassFactory.GetFastAbiClassForInterface(context.Cache, type); + bool isFastAbiDefault = fastAbi is not null && fastAbi.Value.Default is not null + && AbiTypeHelpers.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 += AbiTypeHelpers.CountMethods(type) + AbiTypeHelpers.GetClassHierarchyIndex(context.Cache, fastAbi!.Value.Class); + foreach (TypeDefinition other in fastAbi.Value.Others) + { + segments.Add((other, slot, false)); + slot += AbiTypeHelpers.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 (AbiTypeHelpers.HasEmittableMembers(seg, segSkipEvents)) + { + hasAnyMember = true; + break; + } + } + + if (!hasAnyMember) + { + return; + } + + writer.WriteLine($$""" + {{(useInternal ? "internal static class " : "public static class ")}}{{nameStripped}}Methods + { + """, isMultiline: true); + + foreach ((TypeDefinition iface, int startSlot, bool segSkipEvents) in segments) + { + AbiMethodBodyFactory.EmitMethodsClassMembersFor(writer, context, iface, startSlot, segSkipEvents); + } + + writer.WriteLine("}"); + } + +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs new file mode 100644 index 0000000000..3d8d4757b2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs @@ -0,0 +1,563 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the IDynamicInterfaceCastable shim implementations for projected interface types. +/// Handles required (inherited) interfaces and the special collection forwarders for +/// IObservableMap, IObservableVector, and BCL-mapped types like IBindableVector. +/// +internal static class AbiInterfaceIDicFactory +{ + /// + /// Emits the IDIC (IDynamicInterfaceCastable) impl class that lets user types implement the projected interface via dynamic dispatch through the projected runtime class instance. + /// + public static void WriteInterfaceIdicImpl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (TypeCategorization.IsExclusiveTo(type) && !context.Settings.IdicExclusiveTo) + { + return; + } + + if (type.GenericParameters.Count > 0) + { + return; + } + + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + + writer.WriteLine(); + writer.WriteLine("[DynamicInterfaceCastableImplementation]"); + InterfaceFactory.WriteGuidAttribute(writer, type); + writer.WriteLine(); + writer.Write($"file interface {nameStripped} : "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine(); + using (writer.WriteBlock()) + { + // Emit DIM bodies that dispatch through the static ABI Methods class. + WriteInterfaceIdicImplMembers(writer, context, type); + writer.WriteLine(); + } + } + + /// + /// Emits explicit-interface DIM (default interface method) implementations for the IDIC + /// file interface. + /// + internal static void WriteInterfaceIdicImplMembers(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + HashSet visited = []; + WriteInterfaceIdicImplMembersForInterface(writer, context, type); + + // Also walk required (inherited) interfaces and emit members for each one. + WriteInterfaceIdicImplMembersForRequiredInterfaces(writer, context, type, visited); + } + + internal static void WriteInterfaceIdicImplMembersForRequiredInterfaces( + IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, HashSet visited) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + TypeDefinition? required = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, impl.Interface); + + if (required is null) + { + continue; + } + + if (!visited.Add(required)) + { + continue; + } + + (string rNs, string rName) = required.Names(); + MappedType? mapped = MappedTypes.Get(rNs, rName); + + if (mapped is { HasCustomMembersOutput: true }) + { + // 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(writer, context, 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 = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, 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. + if (rNs == WindowsFoundationCollections && rName == "IObservableMap`2") + { + if (impl.Interface is TypeSpecification tsMap && tsMap.Signature is GenericInstanceTypeSignature giMap && giMap.TypeArguments.Count == 2) + { + string keyText = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(giMap.TypeArguments[0]), TypedefNameType.Projected, true); + string valueText = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(giMap.TypeArguments[1]), TypedefNameType.Projected, true); + EmitDicShimIObservableMapForwarders(writer, context, 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 = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, impl2.Interface); + + if (r2 is not null) + { + _ = visited.Add(r2); + } + } + } + + continue; + } + + if (rNs == WindowsFoundationCollections && rName == "IObservableVector`1") + { + if (impl.Interface is TypeSpecification tsVec && tsVec.Signature is GenericInstanceTypeSignature giVec && giVec.TypeArguments.Count == 1) + { + string elementText = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(giVec.TypeArguments[0]), TypedefNameType.Projected, true); + EmitDicShimIObservableVectorForwarders(writer, context, elementText); + foreach (InterfaceImplementation impl2 in required.Interfaces) + { + if (impl2.Interface is null) + { + continue; + } + + TypeDefinition? r2 = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, 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(writer, context, required, visited); + WriteInterfaceIdicImplMembersForInheritedInterface(writer, context, 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>. + /// write_dictionary_members_using_idic(true) + the IObservableMap event forwarder. + /// + internal static void EmitDicShimIObservableMapForwarders(IndentedTextWriter writer, ProjectionEmitContext context, 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>."; + writer.WriteLine(); + writer.WriteLine($$""" + ICollection<{{keyText}}> {{self}}Keys => {{target}}.Keys; + ICollection<{{valueText}}> {{self}}Values => {{target}}.Values; + int {{icoll}}Count => {{target}}.Count; + bool {{icoll}}IsReadOnly => {{target}}.IsReadOnly; + {{valueText}} {{self}}this[{{keyText}} key] + { + get => {{target}}[key]; + set => {{target}}[key] = value; + } + void {{self}}Add({{keyText}} key, {{valueText}} value) => {{target}}.Add(key, value); + bool {{self}}ContainsKey({{keyText}} key) => {{target}}.ContainsKey(key); + bool {{self}}Remove({{keyText}} key) => {{target}}.Remove(key); + bool {{self}}TryGetValue({{keyText}} key, out {{valueText}} value) => {{target}}.TryGetValue(key, out value); + void {{icoll}}Add(KeyValuePair<{{keyText}}, {{valueText}}> item) => {{target}}.Add(item); + void {{icoll}}Clear() => {{target}}.Clear(); + bool {{icoll}}Contains(KeyValuePair<{{keyText}}, {{valueText}}> item) => {{target}}.Contains(item); + void {{icoll}}CopyTo(KeyValuePair<{{keyText}}, {{valueText}}>[] array, int arrayIndex) => {{target}}.CopyTo(array, arrayIndex); + bool ICollection>.Remove(KeyValuePair<{{keyText}}, {{valueText}}> item) => {{target}}.Remove(item); + IEnumerator> IEnumerable>.GetEnumerator() => {{target}}.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + """, isMultiline: true); + // IObservableMap.MapChanged event forwarder. + string obsTarget = $"((global::Windows.Foundation.Collections.IObservableMap<{keyText}, {valueText}>)(WindowsRuntimeObject)this)"; + string obsSelf = $"global::Windows.Foundation.Collections.IObservableMap<{keyText}, {valueText}>."; + writer.WriteLine(); + writer.WriteLine($$""" + event global::Windows.Foundation.Collections.MapChangedEventHandler<{{keyText}}, {{valueText}}> {{obsSelf}}MapChanged + { + add => {{obsTarget}}.MapChanged += value; + remove => {{obsTarget}}.MapChanged -= value; + } + """, isMultiline: true); + } + + /// + /// Emits IList<T> / ICollection<T> / IEnumerable<T> + + /// IObservableVector<T>.VectorChanged forwarders for a DIC file interface that inherits + /// from Windows.Foundation.Collections.IObservableVector<T>. + /// write_list_members_using_idic(true) + the IObservableVector event forwarder. + /// + internal static void EmitDicShimIObservableVectorForwarders(IndentedTextWriter writer, ProjectionEmitContext context, 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}>."; + writer.WriteLine(); + writer.WriteLine($$""" + int {{icoll}}Count => {{target}}.Count; + bool {{icoll}}IsReadOnly => {{target}}.IsReadOnly; + {{elementText}} {{self}}this[int index] + { + get => {{target}}[index]; + set => {{target}}[index] = value; + } + int {{self}}IndexOf({{elementText}} item) => {{target}}.IndexOf(item); + void {{self}}Insert(int index, {{elementText}} item) => {{target}}.Insert(index, item); + void {{self}}RemoveAt(int index) => {{target}}.RemoveAt(index); + void {{icoll}}Add({{elementText}} item) => {{target}}.Add(item); + void {{icoll}}Clear() => {{target}}.Clear(); + bool {{icoll}}Contains({{elementText}} item) => {{target}}.Contains(item); + void {{icoll}}CopyTo({{elementText}}[] array, int arrayIndex) => {{target}}.CopyTo(array, arrayIndex); + bool {{icoll}}Remove({{elementText}} item) => {{target}}.Remove(item); + IEnumerator<{{elementText}}> IEnumerable<{{elementText}}>.GetEnumerator() => {{target}}.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + """, isMultiline: true); + // IObservableVector.VectorChanged event forwarder. + string obsTarget = $"((global::Windows.Foundation.Collections.IObservableVector<{elementText}>)(WindowsRuntimeObject)this)"; + string obsSelf = $"global::Windows.Foundation.Collections.IObservableVector<{elementText}>."; + writer.WriteLine(); + writer.WriteLine($$""" + event global::Windows.Foundation.Collections.VectorChangedEventHandler<{{elementText}}> {{obsSelf}}VectorChanged + { + add => {{obsTarget}}.VectorChanged += value; + remove => {{obsTarget}}.VectorChanged -= value; + } + """, isMultiline: true); + } + + /// + /// 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. + /// + internal static void WriteInterfaceIdicImplMembersForInheritedInterface(IndentedTextWriter writer, ProjectionEmitContext context, 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 = TypedefNameWriter.WriteTypedefName(context, type, TypedefNameType.Projected, true); + + if (!ccwIfaceName.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + ccwIfaceName = GlobalPrefix + ccwIfaceName; + } + + foreach (MethodDefinition method in type.Methods) + { + if (method.IsSpecial()) + { + continue; + } + + MethodSignatureInfo sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + + writer.WriteLine(); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {ccwIfaceName}.{mname}("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.Write($") => (({ccwIfaceName})(WindowsRuntimeObject)this).{mname}("); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + ClassMembersFactory.WriteParameterNameWithModifier(writer, context, sig.Parameters[i]); + } + writer.WriteLine(");"); + } + + foreach (PropertyDefinition prop in type.Properties) + { + (MethodDefinition? getter, MethodDefinition? setter) = prop.GetPropertyMethods(); + string pname = prop.Name?.Value ?? string.Empty; + string propType = InterfaceFactory.WritePropType(context, prop); + + writer.WriteLine(); + writer.Write($"{propType} {ccwIfaceName}.{pname}"); + + if (getter is not null && setter is null) + { + // Read-only: single-line expression body. + writer.WriteLine($" => (({ccwIfaceName})(WindowsRuntimeObject)this).{pname};"); + } + else + { + writer.WriteLine(); + using (writer.WriteBlock()) + { + if (getter is not null) + { + writer.WriteLine($"get => (({ccwIfaceName})(WindowsRuntimeObject)this).{pname};"); + } + + if (setter is not null) + { + writer.WriteLine($"set => (({ccwIfaceName})(WindowsRuntimeObject)this).{pname} = value;"); + } + } + } + } + + foreach (EventDefinition evt in type.Events) + { + string evtName = evt.Name?.Value ?? string.Empty; + writer.WriteLine(); + writer.Write("event "); + TypedefNameWriter.WriteEventType(writer, context, evt); + writer.WriteLine($$""" + {{ccwIfaceName}}.{{evtName}} + { + add => (({{ccwIfaceName}})(WindowsRuntimeObject)this).{{evtName}} += value; + remove => (({{ccwIfaceName}})(WindowsRuntimeObject)this).{{evtName}} -= value; + } + """, isMultiline: true); + } + } + + /// + /// 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. + /// + internal static void EmitDicShimMappedBclForwarders(IndentedTextWriter writer, ProjectionEmitContext context, string mappedWinRTInterfaceName) + { + switch (mappedWinRTInterfaceName) + { + case "IClosable": + // IClosable maps to IDisposable. Forward Dispose() to the + // WindowsRuntimeObject base which has the actual implementation. + writer.WriteLine(); + writer.WriteLine("void global::System.IDisposable.Dispose() => ((global::System.IDisposable)(WindowsRuntimeObject)this).Dispose();"); + break; + case "IBindableVector": + // IList covers IList, ICollection, and IEnumerable members. + writer.WriteLine(); + writer.WriteLine(""" + int global::System.Collections.ICollection.Count => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Count; + bool global::System.Collections.ICollection.IsSynchronized => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsSynchronized; + object global::System.Collections.ICollection.SyncRoot => ((global::System.Collections.IList)(WindowsRuntimeObject)this).SyncRoot; + void global::System.Collections.ICollection.CopyTo(Array array, int index) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).CopyTo(array, index); + + object global::System.Collections.IList.this[int index] + { + get => ((global::System.Collections.IList)(WindowsRuntimeObject)this)[index]; + set => ((global::System.Collections.IList)(WindowsRuntimeObject)this)[index] = value; + } + bool global::System.Collections.IList.IsFixedSize => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsFixedSize; + bool global::System.Collections.IList.IsReadOnly => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IsReadOnly; + int global::System.Collections.IList.Add(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Add(value); + void global::System.Collections.IList.Clear() => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Clear(); + bool global::System.Collections.IList.Contains(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Contains(value); + int global::System.Collections.IList.IndexOf(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).IndexOf(value); + void global::System.Collections.IList.Insert(int index, object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Insert(index, value); + void global::System.Collections.IList.Remove(object value) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).Remove(value); + void global::System.Collections.IList.RemoveAt(int index) => ((global::System.Collections.IList)(WindowsRuntimeObject)this).RemoveAt(index); + + IEnumerator IEnumerable.GetEnumerator() => ((global::System.Collections.IList)(WindowsRuntimeObject)this).GetEnumerator(); + """, isMultiline: true); + break; + case "IBindableIterable": + writer.WriteLine(); + writer.WriteLine("IEnumerator IEnumerable.GetEnumerator() => ((global::System.Collections.IEnumerable)(WindowsRuntimeObject)this).GetEnumerator();"); + break; + } + } + + internal static void WriteInterfaceIdicImplMembersForInterface(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // The CCW interface name (the projected interface name with global:: prefix). + string ccwIfaceName = TypedefNameWriter.WriteTypedefName(context, type, TypedefNameType.Projected, true); + + if (!ccwIfaceName.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + ccwIfaceName = GlobalPrefix + ccwIfaceName; + } + + // The static ABI Methods class name. + string abiClass = TypedefNameWriter.WriteTypedefName(context, type, TypedefNameType.StaticAbiClass, true); + + if (!abiClass.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + abiClass = GlobalPrefix + abiClass; + } + + foreach (MethodDefinition method in type.Methods) + { + if (method.IsSpecial()) + { + continue; + } + + MethodSignatureInfo sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + + writer.WriteLine(); + writer.Write("unsafe "); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {ccwIfaceName}.{mname}("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.WriteLine($$""" + ) + { + var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof({{ccwIfaceName}}).TypeHandle); + + """, isMultiline: true); + if (sig.ReturnType is not null) + { + writer.Write("return "); + } + + writer.Write($"{abiClass}.{mname}(_obj"); + for (int i = 0; i < sig.Parameters.Count; i++) + { + writer.Write(", "); + ClassMembersFactory.WriteParameterNameWithModifier(writer, context, sig.Parameters[i]); + } + writer.WriteLine(""" + ); + } + """, isMultiline: true); + } + + foreach (PropertyDefinition prop in type.Properties) + { + (MethodDefinition? getter, MethodDefinition? setter) = prop.GetPropertyMethods(); + string pname = prop.Name?.Value ?? string.Empty; + string propType = InterfaceFactory.WritePropType(context, prop); + + writer.WriteLine(); + writer.WriteLine($$""" + unsafe {{propType}} {{ccwIfaceName}}.{{pname}} + { + """, isMultiline: true); + if (getter is not null) + { + writer.WriteLine($$""" + get + { + var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof({{ccwIfaceName}}).TypeHandle); + return {{abiClass}}.{{pname}}(_obj); + } + """, isMultiline: true); + } + + 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 + if (getter is null) + { + TypeDefinition? baseIfaceWithGetter = InterfaceFactory.FindPropertyInterfaceInBases(context.Cache, type, pname); + + if (baseIfaceWithGetter is not null) + { + writer.Write(" get { return (("); + ClassMembersFactory.WriteInterfaceTypeNameForCcw(writer, context, baseIfaceWithGetter); + writer.WriteLine($")(WindowsRuntimeObject)this).{pname}; }}"); + } + } + + writer.WriteLine($$""" + set + { + var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof({{ccwIfaceName}}).TypeHandle); + {{abiClass}}.{{pname}}(_obj, value); + } + """, isMultiline: true); + } + writer.WriteLine("}"); + } + + // Events: emit explicit interface event implementations on the IDIC interface that + // dispatch through the static ABI Methods class's event accessor (returns an EventSource). + foreach (EventDefinition evt in type.Events) + { + string evtName = evt.Name?.Value ?? string.Empty; + writer.WriteLine(); + writer.Write("event "); + TypedefNameWriter.WriteEventType(writer, context, evt); + writer.WriteLine($$""" + {{ccwIfaceName}}.{{evtName}} + { + add + { + var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof({{ccwIfaceName}}).TypeHandle); + {{abiClass}}.{{evtName}}((WindowsRuntimeObject)this, _obj).Subscribe(value); + } + remove + { + var _obj = ((WindowsRuntimeObject)this).GetObjectReferenceForInterface(typeof({{ccwIfaceName}}).TypeHandle); + {{abiClass}}.{{evtName}}((WindowsRuntimeObject)this, _obj).Unsubscribe(value); + } + } + """, isMultiline: true); + } + } + +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs new file mode 100644 index 0000000000..fb5938e4e6 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs @@ -0,0 +1,902 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class AbiMethodBodyFactory +{ + /// + /// Emits a real Do_Abi (CCW) body for the cases we can handle. This is a partial + /// implementation that uses simple per-marshaller patterns inline rather than the + /// fully-general abi_marshaler abstraction. + /// + internal static void EmitDoAbiBodyIfSimple(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig, string ifaceFullName, string methodName) + { + TypeSignature? rt = sig.ReturnType; + + // String params drive whether we need HString header allocation in the body. + bool hasStringParams = false; + foreach (ParameterInfo p in sig.Parameters) + { + if (p.Type.IsString()) + { + hasStringParams = true; + break; + } + } + bool returnIsReceiveArrayDoAbi = rt is SzArrayTypeSignature retSzAbi + && (context.AbiTypeShapeResolver.IsBlittablePrimitive(retSzAbi.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(retSzAbi.BaseType) + || retSzAbi.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSzAbi.BaseType) || retSzAbi.BaseType.IsObject() + || context.AbiTypeShapeResolver.IsComplexStruct(retSzAbi.BaseType)); + bool returnIsHResultExceptionDoAbi = rt is not null && rt.IsHResultException(); + bool returnIsString = rt is not null && rt.IsString(); + bool returnIsRefType = rt is not null && (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(rt) || rt.IsObject() || rt.IsGenericInstance()); + bool returnIsGenericInstance = rt is not null && rt.IsGenericInstance(); + bool returnIsBlittableStruct = rt is not null && context.AbiTypeShapeResolver.IsBlittableStruct(rt); + + bool isGetter = methodName.StartsWith("get_", StringComparison.Ordinal); + bool isSetter = methodName.StartsWith("put_", StringComparison.Ordinal); + bool isAddEvent = methodName.StartsWith("add_", StringComparison.Ordinal); + bool isRemoveEvent = methodName.StartsWith("remove_", StringComparison.Ordinal); + + if (isAddEvent || isRemoveEvent) + { + // Events go through dedicated EmitDoAbiAddEvent / EmitDoAbiRemoveEvent paths + // upstream. If we reach here for an event accessor it's a generator bug. + // Defensive guard against future regressions. + throw WellKnownProjectionWriterExceptions.UnreachableEmissionState( + $"EmitDoAbiBodyIfSimple: unexpectedly called for event accessor '{methodName}' " + + $"on '{ifaceFullName}'. Events should dispatch through EmitDoAbiAddEvent / EmitDoAbiRemoveEvent."); + } + + writer.WriteLine(); + using (writer.WriteBlock()) + { + string retParamName = AbiTypeHelpers.GetReturnParamName(sig); + string retSizeParamName = AbiTypeHelpers.GetReturnSizeParamName(sig); + // The local name for the unmarshalled return value uses the standard pattern + // of prefixing '__' to the param name. For the default '__return_value__' param + // this becomes '____return_value__'. + string retLocalName = "__" + retParamName; + // 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 && rt.IsNullableT())) + { + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(rt!, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, rt!, false); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToUnmanaged")] + static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_{{retParamName}}([UnsafeAccessorType("{{interopTypeName}}")] object _, {{projectedTypeName}} value); + """, isMultiline: true); + writer.WriteLine(); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + TypeSignature uOut = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (!uOut.IsGenericInstance()) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(uOut, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, uOut, false); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToUnmanaged")] + static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_{{raw}}([UnsafeAccessorType("{{interopTypeName}}")] object _, {{projectedTypeName}} value); + """, isMultiline: true); + writer.WriteLine(); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.ReceiveArray) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + SzArrayTypeSignature sza = (SzArrayTypeSignature)AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(sza.BaseType)); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + string marshallerPath = ArrayElementEncoder.GetArrayMarshallerInteropPath(sza.BaseType); + string elementAbi = sza.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(sza.BaseType) || sza.BaseType.IsObject() + ? "void*" + : context.AbiTypeShapeResolver.IsComplexStruct(sza.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, sza.BaseType) + : context.AbiTypeShapeResolver.IsBlittableStruct(sza.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, sza.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, sza.BaseType); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToUnmanaged")] + static extern void ConvertToUnmanaged_{{raw}}([UnsafeAccessorType("{{marshallerPath}}")] object _, ReadOnlySpan<{{elementProjected}}> span, out uint length, out {{elementAbi}}* data); + """, isMultiline: true); + writer.WriteLine(); + } + + if (returnIsReceiveArrayDoAbi && rt is SzArrayTypeSignature retSzHoist) + { + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(retSzHoist.BaseType)); + string elementAbi = retSzHoist.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSzHoist.BaseType) || retSzHoist.BaseType.IsObject() + ? "void*" + : context.AbiTypeShapeResolver.IsComplexStruct(retSzHoist.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, retSzHoist.BaseType) + : context.AbiTypeShapeResolver.IsBlittableStruct(retSzHoist.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, retSzHoist.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, retSzHoist.BaseType); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(retSzHoist.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + string marshallerPath = ArrayElementEncoder.GetArrayMarshallerInteropPath(retSzHoist.BaseType); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToUnmanaged")] + static extern void ConvertToUnmanaged_{{retParamName}}([UnsafeAccessorType("{{marshallerPath}}")] object _, ReadOnlySpan<{{elementProjected}}> span, out uint length, out {{elementAbi}}* data); + """, isMultiline: true); + writer.WriteLine(); + } + + // the OUT pointer(s). The actual assignment happens inside the try block. + if (rt is not null) + { + if (returnIsString) + { + writer.WriteLine($"string {retLocalName} = default;"); + } + else if (returnIsRefType) + { + string projected = MethodFactory.WriteProjectedSignature(context, rt, false); + writer.WriteLine($"{projected} {retLocalName} = default;"); + } + else if (returnIsReceiveArrayDoAbi) + { + string projected = MethodFactory.WriteProjectedSignature(context, rt, false); + writer.WriteLine($"{projected} {retLocalName} = default;"); + } + else + { + string projected = MethodFactory.WriteProjectedSignature(context, rt, false); + writer.WriteLine($"{projected} {retLocalName} = default;"); + } + } + + if (rt is not null) + { + if (returnIsReceiveArrayDoAbi) + { + writer.WriteLine($$""" + *{{retParamName}} = default; + *{{retSizeParamName}} = default; + """, isMultiline: true); + } + else + { + writer.WriteLine($"*{retParamName} = default;"); + } + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.WriteLine($"*{ptr} = default;"); + } + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.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. + TypeSignature underlying = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + string projected = MethodFactory.WriteProjectedSignature(context, underlying, false); + writer.WriteLine($"{projected} __{raw} = default;"); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.ReceiveArray) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + SzArrayTypeSignature sza = (SzArrayTypeSignature)AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(sza.BaseType)); + writer.WriteLine($$""" + *{{ptr}} = default; + *__{{raw}}Size = default; + {{elementProjected}}[] __{{raw}} = default; + """, isMultiline: true); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature sz) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(sz.BaseType)); + bool isBlittableElem = context.AbiTypeShapeResolver.IsBlittablePrimitive(sz.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(sz.BaseType); + + if (isBlittableElem) + { + writer.WriteLine($"{(cat == ParameterCategory.PassArray ? "ReadOnlySpan<" : "Span<")}{elementProjected}> __{raw} = new({ptr}, (int)__{raw}Size);"); + } + else + { + // Non-blittable element: InlineArray16 + ArrayPool with size from ABI. + writer.WriteLine(); + writer.WriteLine($$""" + Unsafe.SkipInit(out InlineArray16<{{elementProjected}}> __{{raw}}_inlineArray); + {{elementProjected}}[] __{{raw}}_arrayFromPool = null; + Span<{{elementProjected}}> __{{raw}} = __{{raw}}Size <= 16 + ? __{{raw}}_inlineArray[..(int)__{{raw}}Size] + : (__{{raw}}_arrayFromPool = global::System.Buffers.ArrayPool<{{elementProjected}}>.Shared.Rent((int)__{{raw}}Size)); + """, isMultiline: true); + } + } + writer.WriteLine(""" + try + { + """, isMultiline: true); + + // 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. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.PassArray) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(szArr.BaseType)); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + // 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 (context.AbiTypeShapeResolver.IsComplexStruct(szArr.BaseType)) + { + string abiStructName = AbiTypeHelpers.GetAbiStructTypeName(writer, context, szArr.BaseType); + dataParamType = abiStructName + "* data"; + dataCastExpr = "(" + abiStructName + "*)" + ptr; + } + else + { + dataParamType = "void** data"; + dataCastExpr = "(void**)" + ptr; + } + + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "CopyToManaged")] + static extern void CopyToManaged_{{raw}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szArr.BaseType)}}")] object _, uint length, {{dataParamType}}, Span<{{elementProjected}}> span); + CopyToManaged_{{raw}}(null, __{{raw}}Size, {{dataCastExpr}}, __{{raw}}); + """, isMultiline: true); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (p.Type.IsNullableT()) + { + // Nullable param (server-side): use Marshaller.UnboxToManaged. + string rawName = p.Parameter.Name ?? "param"; + string callName = CSharpKeywords.IsKeyword(rawName) ? "@" + rawName : rawName; + TypeSignature inner = p.Type.GetNullableInnerType()!; + string innerMarshaller = AbiTypeHelpers.GetNullableInnerMarshallerName(writer, context, inner); + writer.WriteLine($" var __arg_{rawName} = {innerMarshaller}.UnboxToManaged({callName});"); + } + else if (p.Type.IsGenericInstance()) + { + string rawName = p.Parameter.Name ?? "param"; + string callName = CSharpKeywords.IsKeyword(rawName) ? "@" + rawName : rawName; + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, p.Type, false); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToManaged")] + static extern {{projectedTypeName}} ConvertToManaged_arg_{{rawName}}([UnsafeAccessorType("{{interopTypeName}}")] object _, void* value); + var __arg_{{rawName}} = ConvertToManaged_arg_{{rawName}}(null, {{callName}}); + """, isMultiline: true); + } + } + + if (returnIsString) + { + writer.Write($" {retLocalName} = "); + } + else if (returnIsRefType) + { + writer.Write($" {retLocalName} = "); + } + else if (returnIsReceiveArrayDoAbi) + { + // For T[] return: assign to existing local. + writer.Write($" {retLocalName} = "); + } + else if (rt is not null) + { + writer.Write($" {retLocalName} = "); + } + else + { + writer.Write(" "); + } + + if (isGetter) + { + string propName = methodName[4..]; + writer.WriteLine($"ComInterfaceDispatch.GetInstance<{ifaceFullName}>((ComInterfaceDispatch*)thisPtr).{propName};"); + } + else if (isSetter) + { + string propName = methodName[4..]; + writer.Write($"ComInterfaceDispatch.GetInstance<{ifaceFullName}>((ComInterfaceDispatch*)thisPtr).{propName} = "); + EmitDoAbiParamArgConversion(writer, context, sig.Parameters[0]); + writer.WriteLine(";"); + } + else + { + writer.Write($"ComInterfaceDispatch.GetInstance<{ifaceFullName}>((ComInterfaceDispatch*)thisPtr).{methodName}("); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(""" + , + + """, isMultiline: true); + } + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat == ParameterCategory.Out) + { + string raw = p.Parameter.Name ?? "param"; + writer.Write($"out __{raw}"); + } + else if (cat == ParameterCategory.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 = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + TypeSignature uRef = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (uRef.IsString()) + { + writer.Write($"HStringMarshaller.ConvertToManaged(*{ptr})"); + } + else if (uRef.IsObject()) + { + writer.Write($"WindowsRuntimeObjectMarshaller.ConvertToManaged(*{ptr})"); + } + else if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(uRef)) + { + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, uRef)}.ConvertToManaged(*{ptr})"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(uRef)) + { + writer.Write($"{AbiTypeHelpers.GetMappedMarshallerName(uRef)}.ConvertToManaged(*{ptr})"); + } + else if (uRef.IsHResultException()) + { + writer.Write($"global::ABI.System.ExceptionMarshaller.ConvertToManaged(*{ptr})"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(uRef)) + { + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, uRef)}.ConvertToManaged(*{ptr})"); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(uRef) || context.AbiTypeShapeResolver.IsBlittablePrimitive(uRef) || context.AbiTypeShapeResolver.IsEnumType(uRef)) + { + // Blittable/almost-blittable: ABI layout matches projected layout. + writer.Write($"*{ptr}"); + } + else + { + writer.Write($"*{ptr}"); + } + } + else if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + string raw = p.Parameter.Name ?? "param"; + writer.Write($"__{raw}"); + } + else if (cat == ParameterCategory.ReceiveArray) + { + string raw = p.Parameter.Name ?? "param"; + writer.Write($"out __{raw}"); + } + else + { + EmitDoAbiParamArgConversion(writer, context, p); + } + } + writer.WriteLine(");"); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + TypeSignature underlying = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + writer.Write($" *{ptr} = "); + // String: HStringMarshaller.ConvertToUnmanaged + if (underlying.IsString()) + { + writer.Write($"HStringMarshaller.ConvertToUnmanaged(__{raw})"); + } + + // Object/runtime class: .ConvertToUnmanaged(...).DetachThisPtrUnsafe() + else if (underlying.IsObject()) + { + writer.Write($"WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(__{raw}).DetachThisPtrUnsafe()"); + } + else if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(underlying)) + { + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, underlying)}.ConvertToUnmanaged(__{raw}).DetachThisPtrUnsafe()"); + } + + // Generic instance (e.g. IEnumerable): use the hoisted UnsafeAccessor + // 'ConvertToUnmanaged_' declared at the top of the method body. + else if (underlying.IsGenericInstance()) + { + writer.Write($"ConvertToUnmanaged_{raw}(null, __{raw}).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 (context.AbiTypeShapeResolver.IsEnumType(underlying)) + { + writer.Write($"__{raw}"); + } + else if (underlying is CorLibTypeSignature corlibBool && + corlibBool.ElementType == ElementType.Boolean) + { + writer.Write($"__{raw}"); + } + else if (underlying is CorLibTypeSignature corlibChar && + corlibChar.ElementType == ElementType.Char) + { + writer.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.write_marshal_from_managed + //: "Marshaller.ConvertToUnmanaged(local)". + else if (context.AbiTypeShapeResolver.IsComplexStruct(underlying)) + { + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, underlying)}.ConvertToUnmanaged(__{raw})"); + } + else + { + writer.Write($"__{raw}"); + } + writer.WriteLine(";"); + } + + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.ReceiveArray) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.WriteLine($" ConvertToUnmanaged_{raw}(null, __{raw}, out *__{raw}Size, out *{ptr});"); + } + + // 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.. + // 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.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.FillArray) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szFA) + { + continue; + } + + // Blittable element types: Span wraps the native buffer; no copy-back needed. + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szFA.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szFA.BaseType)) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string ptr = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(szFA.BaseType)); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szFA.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + // 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 (szFA.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(szFA.BaseType) || szFA.BaseType.IsObject()) + { + dataParamType = "void** data"; + dataCastType = "(void**)"; + } + else if (szFA.BaseType.IsHResultException()) + { + dataParamType = "global::ABI.System.Exception* data"; + dataCastType = "(global::ABI.System.Exception*)"; + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(szFA.BaseType)) + { + string abiName = AbiTypeHelpers.GetMappedAbiTypeName(szFA.BaseType); + dataParamType = abiName + "* data"; + dataCastType = "(" + abiName + "*)"; + } + else + { + string abiStructName = AbiTypeHelpers.GetAbiStructTypeName(writer, context, szFA.BaseType); + dataParamType = abiStructName + "* data"; + dataCastType = "(" + abiStructName + "*)"; + } + + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "CopyToUnmanaged")] + static extern void CopyToUnmanaged_{{raw}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szFA.BaseType)}}")] object _, ReadOnlySpan<{{elementProjected}}> span, uint length, {{dataParamType}}); + CopyToUnmanaged_{{raw}}(null, __{{raw}}, __{{raw}}Size, {{dataCastType}}{{ptr}}); + """, isMultiline: true); + } + + if (rt is not null) + { + if (returnIsHResultExceptionDoAbi) + { + writer.WriteLine($" *{retParamName} = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged({retLocalName});"); + } + else if (returnIsString) + { + writer.WriteLine($" *{retParamName} = HStringMarshaller.ConvertToUnmanaged({retLocalName});"); + } + else if (returnIsRefType) + { + if (rt is not null && rt.IsNullableT()) + { + // Nullable return (server-side): use Marshaller.BoxToUnmanaged. + TypeSignature inner = rt.GetNullableInnerType()!; + string innerMarshaller = AbiTypeHelpers.GetNullableInnerMarshallerName(writer, context, inner); + writer.WriteLine($" *{retParamName} = {innerMarshaller}.BoxToUnmanaged({retLocalName}).DetachThisPtrUnsafe();"); + } + 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. + writer.WriteLine($" *{retParamName} = ConvertToUnmanaged_{retParamName}(null, {retLocalName}).DetachThisPtrUnsafe();"); + } + else + { + writer.Write($" *{retParamName} = "); + EmitMarshallerConvertToUnmanaged(writer, context, rt!, retLocalName); + writer.WriteLine(".DetachThisPtrUnsafe();"); + } + } + else if (returnIsReceiveArrayDoAbi) + { + // Return-receive-array: emit ConvertToUnmanaged_ call (declaration + // was hoisted to the top of the method body). + writer.WriteLine($" ConvertToUnmanaged_{retParamName}(null, {retLocalName}, out *{retSizeParamName}, out *{retParamName});"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(rt)) + { + // Mapped value type return (DateTime/TimeSpan): convert via marshaller. + writer.WriteLine($" *{retParamName} = {AbiTypeHelpers.GetMappedMarshallerName(rt)}.ConvertToUnmanaged({retLocalName});"); + } + else if (rt.IsSystemType()) + { + // System.Type return (server-side): convert managed System.Type to ABI Type struct. + writer.WriteLine($" *{retParamName} = global::ABI.System.TypeMarshaller.ConvertToUnmanaged({retLocalName});"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(rt)) + { + // Complex struct return (server-side): convert managed struct to ABI struct via marshaller. + writer.WriteLine($" *{retParamName} = {AbiTypeHelpers.GetMarshallerFullName(writer, context, rt)}.ConvertToUnmanaged({retLocalName});"); + } + else if (returnIsBlittableStruct) + { + writer.WriteLine($" *{retParamName} = {retLocalName};"); + } + else + { + writer.Write($" *{retParamName} = "); + + if (rt is CorLibTypeSignature corlib && + corlib.ElementType == ElementType.Boolean) + { + writer.WriteLine($"{retLocalName};"); + } + else if (rt is CorLibTypeSignature corlib2 && + corlib2.ElementType == ElementType.Char) + { + writer.WriteLine($"{retLocalName};"); + } + else if (context.AbiTypeShapeResolver.IsEnumType(rt)) + { + // Enum: function pointer signature uses the projected enum type, no cast needed. + writer.WriteLine($"{retLocalName};"); + } + else + { + writer.WriteLine($"{retLocalName};"); + } + } + } + + writer.WriteLine(""" + return 0; + } + catch (Exception __exception__) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__); + } + """, isMultiline: true); + + // For non-blittable PassArray params, emit finally block with ArrayPool.Shared.Return. + bool hasNonBlittableArrayDoAbi = false; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + hasNonBlittableArrayDoAbi = true; + break; + } + + if (hasNonBlittableArrayDoAbi) + { + writer.WriteLine(""" + finally + { + """, isMultiline: true); + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(szArr.BaseType)); + writer.WriteLine(); + writer.WriteLine($$""" + if (__{{raw}}_arrayFromPool is not null) + { + global::System.Buffers.ArrayPool<{{elementProjected}}>.Shared.Return(__{{raw}}_arrayFromPool); + } + """, isMultiline: true); + } + writer.WriteLine("}"); + } + } + writer.WriteLine(); + _ = hasStringParams; + } + + /// + /// Converts an ABI parameter to its projected (managed) form for the Do_Abi call. + /// + internal static void EmitDoAbiParamArgConversion(IndentedTextWriter writer, ProjectionEmitContext context, ParameterInfo p) + { + string rawName = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(rawName) ? "@" + rawName : rawName; + + if (p.Type is CorLibTypeSignature corlib && + corlib.ElementType == ElementType.Boolean) + { + writer.Write(pname); + } + else if (p.Type is CorLibTypeSignature corlib2 && + corlib2.ElementType == ElementType.Char) + { + writer.Write(pname); + } + else if (p.Type is CorLibTypeSignature corlibStr && + corlibStr.ElementType == ElementType.String) + { + writer.Write($"HStringMarshaller.ConvertToManaged({pname})"); + } + else if (p.Type.IsGenericInstance()) + { + // Generic instance ABI parameter: caller already declared a local UnsafeAccessor + + // local var __arg_ that holds the converted value. + writer.Write($"__arg_{rawName}"); + } + else if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(p.Type) || p.Type.IsObject()) + { + EmitMarshallerConvertToManaged(writer, context, p.Type, pname); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(p.Type)) + { + // Mapped value type input (DateTime/TimeSpan): the parameter is the ABI type; + // convert to the projected managed type via the marshaller. + writer.Write($"{AbiTypeHelpers.GetMappedMarshallerName(p.Type)}.ConvertToManaged({pname})"); + } + else if (p.Type.IsSystemType()) + { + // System.Type input (server-side): convert ABI Type struct to System.Type. + writer.Write($"global::ABI.System.TypeMarshaller.ConvertToManaged({pname})"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(p.Type)) + { + // Complex struct input (server-side): convert ABI struct to managed via marshaller. + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, p.Type)}.ConvertToManaged({pname})"); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(p.Type)) + { + // Blittable / almost-blittable struct: pass directly (projected type == ABI type). + writer.Write(pname); + } + else if (context.AbiTypeShapeResolver.IsEnumType(p.Type)) + { + // Enum: param signature is already the projected enum type, no cast needed. + writer.Write(pname); + } + else + { + writer.Write(pname); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.MarshallerDispatch.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.MarshallerDispatch.cs new file mode 100644 index 0000000000..dbf54502a7 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.MarshallerDispatch.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class AbiMethodBodyFactory +{ + /// + /// Emits the call to the appropriate marshaller's ConvertToUnmanaged for a runtime class / object input parameter. + /// + internal static void EmitMarshallerConvertToUnmanaged(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature sig, string argName) + { + if (sig.IsObject()) + { + writer.Write($"WindowsRuntimeObjectMarshaller.ConvertToUnmanaged({argName})"); + return; + } + + // Runtime class / interface: use ABI..Marshaller + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, sig)}.ConvertToUnmanaged({argName})"); + } + + /// + /// Emits the call to the appropriate marshaller's ConvertToManaged for a runtime class / object return value. + /// + internal static void EmitMarshallerConvertToManaged(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature sig, string argName) + { + if (sig.IsObject()) + { + writer.Write($"WindowsRuntimeObjectMarshaller.ConvertToManaged({argName})"); + return; + } + + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, sig)}.ConvertToManaged({argName})"); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.MethodsClass.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.MethodsClass.cs new file mode 100644 index 0000000000..8e43b0d668 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.MethodsClass.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class AbiMethodBodyFactory +{ + /// + /// 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. + /// behavior of inserting write_unsafe_accessor_for_iid at the top of the class body. + /// + internal static void EmitUnsafeAccessorForDefaultIfaceIfGeneric(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef? defaultIface) + { + if (defaultIface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ObjRefNameGenerator.EmitUnsafeAccessorForIid(writer, context, gi); + } + } + + /// + /// 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. + /// + internal static void EmitMethodsClassMembersFor(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, int startSlot, bool skipExclusiveEvents) + { + // Build a map from each MethodDefinition to its WinMD vtable 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 = []; + 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 (method.IsSpecial()) + { + continue; + } + + string mname = method.Name?.Value ?? string.Empty; + MethodSignatureInfo sig = new(method); + + writer.Write(""" + [MethodImpl(MethodImplOptions.NoInlining)] + public static unsafe + """, isMultiline: true); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {mname}(WindowsRuntimeObjectReference thisReference"); + + if (sig.Parameters.Count > 0) + { + writer.Write(", "); + } + + MethodFactory.WriteParameterList(writer, context, sig); + writer.Write(")"); + + // Emit the body if we can handle this case. Slot comes from the method's WinMD index. + EmitAbiMethodBodyIfSimple(writer, context, sig, methodSlot[method], isNoExcept: method.IsNoExcept()); + } + + // 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) = prop.GetPropertyMethods(); + string propType = InterfaceFactory.WritePropType(context, prop); + (MethodDefinition? gMethod, MethodDefinition? sMethod) = (getter, setter); + // accessors of the property (the attribute is on the property itself, not on the + // individual accessors). + bool propIsNoExcept = prop.IsNoExcept(); + + if (gMethod is not null) + { + MethodSignatureInfo getSig = new(gMethod); + writer.Write($$""" + [MethodImpl(MethodImplOptions.NoInlining)] + public static unsafe {{propType}} {{pname}}(WindowsRuntimeObjectReference thisReference) + """, isMultiline: true); + EmitAbiMethodBodyIfSimple(writer, context, getSig, methodSlot[gMethod], isNoExcept: propIsNoExcept); + } + + if (sMethod is not null) + { + MethodSignatureInfo setSig = new(sMethod); + writer.Write($$""" + [MethodImpl(MethodImplOptions.NoInlining)] + public static unsafe void {{pname}}(WindowsRuntimeObjectReference thisReference, {{InterfaceFactory.WritePropType(context, prop, isSetProperty: true)}} value) + """, isMultiline: true); + EmitAbiMethodBodyIfSimple(writer, context, 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. + foreach (EventDefinition evt in type.Events) + { + if (skipExclusiveEvents) + { + continue; + } + + string evtName = evt.Name?.Value ?? string.Empty; + TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); + bool isGenericEvent = evtSig is GenericInstanceTypeSignature; + + // Use the add method's WinMD slot (events project as the add_X method's vmethod_index). + (MethodDefinition? addMethod, MethodDefinition? _) = evt.GetEventMethods(); + 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 = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(evtSig), TypedefNameType.EventSource, true); + + if (!eventSourceProjectedFull.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + eventSourceProjectedFull = GlobalPrefix + 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 + string delegateName = string.Empty; + + if (evtSig is TypeDefOrRefSignature td) + { + delegateName = td.Type?.Name?.Value ?? string.Empty; + delegateName = IdentifierEscaping.StripBackticks(delegateName); + } + + eventSourceProjectedFull = delegateName + "EventSource"; + } + + string eventSourceInteropType = isGenericEvent + ? InteropTypeNameWriter.EncodeInteropTypeName(evtSig, TypedefNameType.EventSource) + ", WinRT.Interop" + : string.Empty; + + // Emit the per-event ConditionalWeakTable static field. + writer.WriteLine(); + writer.WriteLine($$""" + private static ConditionalWeakTable _{{evtName}} + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + [MethodImpl(MethodImplOptions.NoInlining)] + static ConditionalWeakTable MakeTable() + { + _ = global::System.Threading.Interlocked.CompareExchange(ref field, [], null); + + return global::System.Threading.Volatile.Read(in field); + } + + return global::System.Threading.Volatile.Read(in field) ?? MakeTable(); + } + } + + public static {{eventSourceProjectedFull}} {{evtName}}(object thisObject, WindowsRuntimeObjectReference thisReference) + { + """, isMultiline: true); + if (isGenericEvent && !string.IsNullOrEmpty(eventSourceInteropType)) + { + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.Constructor)] + [return: UnsafeAccessorType("{{eventSourceInteropType}}")] + static extern object ctor(WindowsRuntimeObjectReference nativeObjectReference, int index); + + return _{{evtName}}.GetOrAdd( + key: thisObject, + valueFactory: static (_, thisReference) => Unsafe.As<{{eventSourceProjectedFull}}>(ctor(thisReference, {{eventSlot.ToString(CultureInfo.InvariantCulture)}})), + factoryArgument: thisReference); + """, isMultiline: true); + } + else + { + // Non-generic delegate: directly construct. + writer.WriteLine($$""" + return _{{evtName}}.GetOrAdd( + key: thisObject, + valueFactory: static (_, thisReference) => new {{eventSourceProjectedFull}}(thisReference, {{eventSlot.ToString(CultureInfo.InvariantCulture)}}), + factoryArgument: thisReference); + """, isMultiline: true); + } + writer.WriteLine(" }"); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs new file mode 100644 index 0000000000..9b7c2232fb --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs @@ -0,0 +1,1697 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class AbiMethodBodyFactory +{ + /// + /// Emits a real method body for the cases we can fully marshal, otherwise emits + /// the 'throw null!' stub. Trailing newline is included. + /// + /// The writer to emit to. + /// The active emit context. + /// The interface method signature being emitted. + /// The vtable slot of the method on the runtime interface. + /// When provided, overrides the default 'thisReference' parameter name (used by FastAbi-merged Methods classes). + /// When true, the vtable call is emitted WITHOUT the + /// RestrictedErrorInfo.ThrowExceptionForHR(...) wrap (methods/properties annotated with + /// [Windows.Foundation.Metadata.NoExceptionAttribute], or remove-overload methods, + /// contractually return S_OK). + [SuppressMessage("Style", "IDE0045:Convert to conditional expression", + Justification = "if/else if chains over type-class predicates are more readable than nested ternaries.")] + internal static void EmitAbiMethodBodyIfSimple(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig, int slot, string? paramNameOverride = null, bool isNoExcept = false) + { + TypeSignature? rt = sig.ReturnType; + + AbiTypeShapeKind returnShape = rt is null ? AbiTypeShapeKind.Unknown : context.AbiTypeShapeResolver.Resolve(rt).Kind; + + bool returnIsString = returnShape == AbiTypeShapeKind.String; + bool returnIsRefType = returnShape is AbiTypeShapeKind.RuntimeClassOrInterface or AbiTypeShapeKind.Delegate or AbiTypeShapeKind.Object or AbiTypeShapeKind.GenericInstance or AbiTypeShapeKind.NullableT; + bool returnIsBlittableStruct = returnShape == AbiTypeShapeKind.BlittableStruct; + bool returnIsComplexStruct = returnShape == AbiTypeShapeKind.ComplexStruct; + bool returnIsReceiveArray = rt is SzArrayTypeSignature retSzCheck + && (context.AbiTypeShapeResolver.IsBlittablePrimitive(retSzCheck.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(retSzCheck.BaseType) + || retSzCheck.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSzCheck.BaseType) || retSzCheck.BaseType.IsObject() + || context.AbiTypeShapeResolver.IsComplexStruct(retSzCheck.BaseType) + || retSzCheck.BaseType.IsHResultException() + || context.AbiTypeShapeResolver.IsMappedAbiValueType(retSzCheck.BaseType)); + bool returnIsHResultException = returnShape == AbiTypeShapeKind.HResultException; + + // Build the function pointer signature: void*, [paramAbiType...,] [retAbiType*,] int + StringBuilder fp = new(); + _ = fp.Append("void*"); + foreach (ParameterInfo p in sig.Parameters) + { + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + _ = fp.Append(", uint, void*"); + continue; + } + + if (cat == ParameterCategory.Out) + { + TypeSignature uOut = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + _ = fp.Append(", "); + + if (uOut.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(uOut) || uOut.IsObject() || uOut.IsGenericInstance()) + { + _ = fp.Append("void**"); + } + else if (uOut.IsSystemType()) + { + _ = fp.Append("global::ABI.System.Type*"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(uOut)) + { + _ = fp.Append(AbiTypeHelpers.GetAbiStructTypeName(writer, context, uOut)); _ = fp.Append('*'); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(uOut)) + { + _ = fp.Append(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, uOut)); _ = fp.Append('*'); + } + else + { + _ = fp.Append(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, uOut)); _ = fp.Append('*'); + } + + continue; + } + + if (cat == ParameterCategory.Ref) + { + TypeSignature uRef = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + _ = fp.Append(", "); + + if (context.AbiTypeShapeResolver.IsComplexStruct(uRef)) + { + _ = fp.Append(AbiTypeHelpers.GetAbiStructTypeName(writer, context, uRef)); _ = fp.Append('*'); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(uRef)) + { + _ = fp.Append(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, uRef)); _ = fp.Append('*'); + } + else + { + _ = fp.Append(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, uRef)); _ = fp.Append('*'); + } + + continue; + } + + if (cat == ParameterCategory.ReceiveArray) + { + SzArrayTypeSignature sza = (SzArrayTypeSignature)AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + _ = fp.Append(", uint*, "); + + if (sza.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(sza.BaseType) || sza.BaseType.IsObject()) + { + _ = fp.Append("void*"); + } + else if (sza.BaseType.IsHResultException()) + { + _ = fp.Append("global::ABI.System.Exception"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(sza.BaseType)) + { + _ = fp.Append(AbiTypeHelpers.GetMappedAbiTypeName(sza.BaseType)); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(sza.BaseType)) + { + _ = fp.Append(AbiTypeHelpers.GetAbiStructTypeName(writer, context, sza.BaseType)); + } + else + { + _ = fp.Append(context.AbiTypeShapeResolver.IsBlittableStruct(sza.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, sza.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, sza.BaseType)); + } + + _ = fp.Append("**"); + continue; + } + + _ = fp.Append(", "); + + if (p.Type.IsHResultException()) + { + _ = fp.Append("global::ABI.System.Exception"); + } + else if (p.Type.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(p.Type) || p.Type.IsObject() || p.Type.IsGenericInstance()) + { + _ = fp.Append("void*"); + } + else if (p.Type.IsSystemType()) + { + _ = fp.Append("global::ABI.System.Type"); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(p.Type)) + { + _ = fp.Append(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, p.Type)); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(p.Type)) + { + _ = fp.Append(AbiTypeHelpers.GetMappedAbiTypeName(p.Type)); + } + else + { + _ = fp.Append(context.AbiTypeShapeResolver.IsComplexStruct(p.Type) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, p.Type) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, p.Type)); + } + } + + if (rt is not null) + { + if (returnIsReceiveArray) + { + SzArrayTypeSignature retSz = (SzArrayTypeSignature)rt; + _ = fp.Append(", uint*, "); + + if (retSz.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSz.BaseType) || retSz.BaseType.IsObject()) + { + _ = fp.Append("void*"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(retSz.BaseType)) + { + _ = fp.Append(AbiTypeHelpers.GetAbiStructTypeName(writer, context, retSz.BaseType)); + } + else if (retSz.BaseType.IsHResultException()) + { + _ = fp.Append("global::ABI.System.Exception"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(retSz.BaseType)) + { + _ = fp.Append(AbiTypeHelpers.GetMappedAbiTypeName(retSz.BaseType)); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(retSz.BaseType)) + { + _ = fp.Append(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, retSz.BaseType)); + } + else + { + _ = fp.Append(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, retSz.BaseType)); + } + + _ = fp.Append("**"); + } + else if (returnIsHResultException) + { + _ = fp.Append(", global::ABI.System.Exception*"); + } + else + { + _ = fp.Append(", "); + + if (returnIsString || returnIsRefType) + { + _ = fp.Append("void**"); + } + else if (rt is not null && rt.IsSystemType()) + { + _ = fp.Append("global::ABI.System.Type*"); + } + else if (returnIsBlittableStruct) + { + _ = fp.Append(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, rt!)); _ = fp.Append('*'); + } + else if (returnIsComplexStruct) + { + _ = fp.Append(AbiTypeHelpers.GetAbiStructTypeName(writer, context, rt!)); _ = fp.Append('*'); + } + else if (rt is not null && context.AbiTypeShapeResolver.IsMappedAbiValueType(rt)) + { + _ = fp.Append(AbiTypeHelpers.GetMappedAbiTypeName(rt)); _ = fp.Append('*'); + } + else + { + _ = fp.Append(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, rt!)); _ = fp.Append('*'); + } + } + } + + _ = fp.Append(", int"); + + writer.WriteLine(); + writer.WriteLine(""" + { + using WindowsRuntimeObjectReferenceValue thisValue = thisReference.AsValue(); + void* ThisPtr = thisValue.GetThisPtrUnsafe(); + """, isMultiline: true); + + // Declare 'using' marshaller values for ref-type parameters (these need disposing). + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(p.Type) || p.Type.IsObject()) + { + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + writer.Write($" using WindowsRuntimeObjectReferenceValue __{localName} = "); + EmitMarshallerConvertToUnmanaged(writer, context, p.Type, callName); + writer.WriteLine(";"); + } + else if (p.Type.IsNullableT()) + { + // Nullable param: use Marshaller.BoxToUnmanaged. + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + TypeSignature inner = p.Type.GetNullableInnerType()!; + string innerMarshaller = AbiTypeHelpers.GetNullableInnerMarshallerName(writer, context, inner); + writer.WriteLine($" using WindowsRuntimeObjectReferenceValue __{localName} = {innerMarshaller}.BoxToUnmanaged({callName});"); + } + else if (p.Type.IsGenericInstance()) + { + // Generic instance param: emit a local UnsafeAccessor delegate to get the marshaller method. + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, p.Type, false); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToUnmanaged")] + static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_{{localName}}([UnsafeAccessorType("{{interopTypeName}}")] object _, {{projectedTypeName}} value); + using WindowsRuntimeObjectReferenceValue __{{localName}} = ConvertToUnmanaged_{{localName}}(null, {{callName}}); + """, isMultiline: true); + } + } + + // (String input params are now stack-allocated via the fast-path pinning pattern below; + // no separate void* local declaration or up-front allocation is needed.) + // Declare locals for HResult/Exception input parameters (converted up-front). + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (ParameterCategoryResolver.GetParamCategory(p) != ParameterCategory.In) + { + continue; + } + + if (!p.Type.IsHResultException()) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + writer.WriteLine($" global::ABI.System.Exception __{localName} = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged({callName});"); + } + + // Declare locals for mapped value-type input parameters (DateTime/TimeSpan): convert via marshaller up-front. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (ParameterCategoryResolver.GetParamCategory(p) != ParameterCategory.In) + { + continue; + } + + if (!context.AbiTypeShapeResolver.IsMappedAbiValueType(p.Type)) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + writer.WriteLine($" {AbiTypeHelpers.GetMappedAbiTypeName(p.Type)} __{localName} = {AbiTypeHelpers.GetMappedMarshallerName(p.Type)}.ConvertToUnmanaged({callName});"); + } + + // Declare locals for complex-struct input parameters (e.g. ProfileUsage with nested + // string/Nullable fields): default-initialize OUTSIDE try, assign inside try via marshaller, + // dispose in finally. + // Includes both 'in' (ParameterCategory.In) and 'in T' (ParameterCategory.Ref) forms. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.In or ParameterCategory.Ref)) + { + continue; + } + + TypeSignature pType = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (!context.AbiTypeShapeResolver.IsComplexStruct(pType)) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + writer.WriteLine($" {AbiTypeHelpers.GetAbiStructTypeName(writer, context, pType)} __{localName} = default;"); + } + + // Declare locals for Out parameters (need to be passed as &__ to the call). + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + TypeSignature uOut = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + writer.Write(" "); + + if (uOut.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(uOut) || uOut.IsObject() || uOut.IsGenericInstance()) + { + writer.Write("void*"); + } + else if (uOut.IsSystemType()) + { + writer.Write("global::ABI.System.Type"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(uOut)) + { + writer.Write(AbiTypeHelpers.GetAbiStructTypeName(writer, context, uOut)); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(uOut)) + { + writer.Write(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, uOut)); + } + else + { + writer.Write(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, uOut)); + } + + writer.WriteLine($" __{localName} = default;"); + } + + // Declare locals for ReceiveArray params (uint length + element pointer). + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.ReceiveArray) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + SzArrayTypeSignature sza = (SzArrayTypeSignature)AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + writer.WriteLine($$""" + uint __{{localName}}_length = default; + + """, isMultiline: true); + // Element ABI type: void* for ref types; ABI struct for complex/blittable structs; + // primitive ABI otherwise. + if (sza.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(sza.BaseType) || sza.BaseType.IsObject()) + { + writer.Write("void*"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(sza.BaseType)) + { + writer.Write(AbiTypeHelpers.GetAbiStructTypeName(writer, context, sza.BaseType)); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(sza.BaseType)) + { + writer.Write(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, sza.BaseType)); + } + else + { + writer.Write(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, sza.BaseType)); + } + + writer.WriteLine($"* __{localName}_data = default;"); + } + + // Declare InlineArray16 + ArrayPool fallback for non-blittable PassArray params + // (runtime classes, objects, strings). Runtime class/object: just one InlineArray16. + // String: also needs InlineArray16 + InlineArray16 for pinned handles. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + // Non-blittable element type: emit InlineArray16 + ArrayPool. + // For mapped value types (DateTime/TimeSpan), use the ABI struct type. + // For complex structs (e.g. authored BasicStruct with reference fields), use the ABI + // struct type. For everything else (runtime classes, objects, strings), use nint. + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string storageT = context.AbiTypeShapeResolver.IsMappedAbiValueType(szArr.BaseType) + ? AbiTypeHelpers.GetMappedAbiTypeName(szArr.BaseType) + : context.AbiTypeShapeResolver.IsComplexStruct(szArr.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, szArr.BaseType) + : szArr.BaseType.IsHResultException() + ? "global::ABI.System.Exception" + : "nint"; + writer.WriteLine(); + writer.WriteLine($$""" + Unsafe.SkipInit(out InlineArray16<{{storageT}}> __{{localName}}_inlineArray); + {{storageT}}[] __{{localName}}_arrayFromPool = null; + Span<{{storageT}}> __{{localName}}_span = {{callName}}.Length <= 16 + ? __{{localName}}_inlineArray[..{{callName}}.Length] + : (__{{localName}}_arrayFromPool = global::System.Buffers.ArrayPool<{{storageT}}>.Shared.Rent({{callName}}.Length)); + """, isMultiline: true); + + if (szArr.BaseType.IsString() && cat == ParameterCategory.PassArray) + { + // Strings need an additional InlineArray16 + InlineArray16 (pinned handles). + // Only required for PassArray (managed -> HSTRING conversion); FillArray's native side + // fills HSTRING handles directly into the nint storage. + writer.WriteLine(); + writer.WriteLine($$""" + Unsafe.SkipInit(out InlineArray16 __{{localName}}_inlineHeaderArray); + HStringHeader[] __{{localName}}_headerArrayFromPool = null; + Span __{{localName}}_headerSpan = {{callName}}.Length <= 16 + ? __{{localName}}_inlineHeaderArray[..{{callName}}.Length] + : (__{{localName}}_headerArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent({{callName}}.Length)); + + Unsafe.SkipInit(out InlineArray16 __{{localName}}_inlinePinnedHandleArray); + nint[] __{{localName}}_pinnedHandleArrayFromPool = null; + Span __{{localName}}_pinnedHandleSpan = {{callName}}.Length <= 16 + ? __{{localName}}_inlinePinnedHandleArray[..{{callName}}.Length] + : (__{{localName}}_pinnedHandleArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent({{callName}}.Length)); + """, isMultiline: true); + } + } + + if (returnIsReceiveArray) + { + SzArrayTypeSignature retSz = (SzArrayTypeSignature)rt!; + writer.WriteLine(""" + uint __retval_length = default; + + """, isMultiline: true); + if (retSz.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSz.BaseType) || retSz.BaseType.IsObject()) + { + writer.Write("void*"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(retSz.BaseType)) + { + writer.Write(AbiTypeHelpers.GetAbiStructTypeName(writer, context, retSz.BaseType)); + } + else if (retSz.BaseType.IsHResultException()) + { + writer.Write("global::ABI.System.Exception"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(retSz.BaseType)) + { + writer.Write(AbiTypeHelpers.GetMappedAbiTypeName(retSz.BaseType)); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(retSz.BaseType)) + { + writer.Write(AbiTypeHelpers.GetBlittableStructAbiType(writer, context, retSz.BaseType)); + } + else + { + writer.Write(AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, retSz.BaseType)); + } + + writer.WriteLine("* __retval_data = default;"); + } + else if (returnIsHResultException) + { + writer.WriteLine(" global::ABI.System.Exception __retval = default;"); + } + else if (returnIsString || returnIsRefType) + { + writer.WriteLine(" void* __retval = default;"); + } + else if (returnIsBlittableStruct) + { + writer.WriteLine($" {AbiTypeHelpers.GetBlittableStructAbiType(writer, context, rt!)} __retval = default;"); + } + else if (returnIsComplexStruct) + { + writer.WriteLine($" {AbiTypeHelpers.GetAbiStructTypeName(writer, context, rt!)} __retval = default;"); + } + else if (rt is not null && context.AbiTypeShapeResolver.IsMappedAbiValueType(rt)) + { + // Mapped value type return (e.g. DateTime/TimeSpan): use the ABI struct as __retval. + writer.WriteLine($" {AbiTypeHelpers.GetMappedAbiTypeName(rt)} __retval = default;"); + } + else if (rt is not null && rt.IsSystemType()) + { + // System.Type return: use ABI Type struct as __retval. + writer.WriteLine(" global::ABI.System.Type __retval = default;"); + } + else if (rt is not null) + { + writer.WriteLine($" {AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, rt)} __retval = default;"); + } + + // Determine if we need a try/finally (for cleanup of string/refType return or receive array + // return or Out runtime class params). Input string params no longer need try/finally — + // they use the HString fast-path (stack-allocated HStringReference, no free needed). + bool hasOutNeedsCleanup = false; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + TypeSignature uOut = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (uOut.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(uOut) || uOut.IsObject() || uOut.IsSystemType() || context.AbiTypeShapeResolver.IsComplexStruct(uOut) || uOut.IsGenericInstance()) + { + hasOutNeedsCleanup = true; + break; + } + } + bool hasReceiveArray = false; + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (ParameterCategoryResolver.GetParamCategory(sig.Parameters[i]) == ParameterCategory.ReceiveArray) + { + hasReceiveArray = true; + break; + } + } + bool hasNonBlittablePassArray = false; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if ((cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + && p.Type is SzArrayTypeSignature szArrCheck + && !context.AbiTypeShapeResolver.IsBlittablePrimitive(szArrCheck.BaseType) && !context.AbiTypeShapeResolver.IsBlittableStruct(szArrCheck.BaseType) + && !context.AbiTypeShapeResolver.IsMappedAbiValueType(szArrCheck.BaseType)) + { + hasNonBlittablePassArray = true; + break; + } + } + bool hasComplexStructInput = false; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if ((cat is ParameterCategory.In or ParameterCategory.Ref) && context.AbiTypeShapeResolver.IsComplexStruct(AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type))) + { + hasComplexStructInput = true; + break; + } + } + + // System.Type return: ABI.System.Type contains an HSTRING that must be disposed + // after marshalling to managed System.Type, otherwise the HSTRING leaks. + bool returnIsSystemTypeForCleanup = rt is not null && rt.IsSystemType(); + bool needsTryFinally = returnIsString || returnIsRefType || returnIsReceiveArray || hasOutNeedsCleanup || hasReceiveArray || returnIsComplexStruct || hasNonBlittablePassArray || hasComplexStructInput || returnIsSystemTypeForCleanup; + + if (needsTryFinally) + { + writer.WriteLine(""" + try + { + """, isMultiline: true); + } + + string indent = needsTryFinally ? " " : " "; + + // Inside try (if applicable): assign complex-struct input locals via marshaller. + //.: '__value = ProfileUsageMarshaller.ConvertToUnmanaged(value);' + // Includes both 'in' (ParameterCategory.In) and 'in T' (ParameterCategory.Ref) forms. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.In or ParameterCategory.Ref)) + { + continue; + } + + TypeSignature pType = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (!context.AbiTypeShapeResolver.IsComplexStruct(pType)) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + writer.WriteLine($"{indent}__{localName} = {AbiTypeHelpers.GetMarshallerFullName(writer, context, pType)}.ConvertToUnmanaged({callName});"); + } + + // Type input params: set up TypeReference locals before the fixed block: + // global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe(forType, out TypeReference __forType); + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (ParameterCategoryResolver.GetParamCategory(p) != ParameterCategory.In) + { + continue; + } + + if (!p.Type.IsSystemType()) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + writer.WriteLine($"{indent}global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe({callName}, out TypeReference __{localName});"); + } + + // Open a SINGLE fixed-block for ALL pinnable inputs: + // 1. Ref params (typed ptr, separate "fixed(T* _x = &x)\n" lines, no braces) + // 2. Complex-struct PassArrays (typed ptr, separate fixed line) + // 3. All other "void*"-style pinnables (strings, Type[], blittable PassArrays, + // reference-type PassArrays via inline-pool span) merged into ONE + // "fixed(void* _a = ..., _b = ..., ...) {\n" block. + // C# allows multiple chained "fixed(...)" without braces to share the next braced + // body, which is what the original code emits. This avoids the deep nesting mine had + // when emitting a separate fixed block per PassArray. + int fixedNesting = 0; + + // Step 1: Emit typed-pointer fixed lines for Ref params and complex-struct PassArrays + // (no braces - they share the body of the upcoming combined fixed-void* block, OR + // each other if no void* block is needed). + bool hasAnyVoidStarPinnable = false; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (p.Type.IsString() || p.Type.IsSystemType()) + { + hasAnyVoidStarPinnable = true; + continue; + } + + if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + // All PassArrays (including complex structs) go in the void* combined block, + // matching truth's pattern. Complex structs use a (T*) cast at the call site. + hasAnyVoidStarPinnable = true; + } + } + + // Emit typed fixed lines for Ref params. + // Skip Ref+ComplexStruct: those are marshalled via __local (no fixed needed) and + // passed as &__local at the call site (the is-value-type-in path). + int typedFixedCount = 0; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat == ParameterCategory.Ref) + { + TypeSignature uRefSkip = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (context.AbiTypeShapeResolver.IsComplexStruct(uRefSkip)) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + TypeSignature uRef = uRefSkip; + string abiType = context.AbiTypeShapeResolver.IsBlittableStruct(uRef) ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, uRef) : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, uRef); + writer.WriteLine($"{indent}{new string(' ', fixedNesting * 4)}fixed({abiType}* _{localName} = &{callName})"); + typedFixedCount++; + } + } + + // Step 2: Emit ONE combined fixed-void* block for all pinnables that share the + // same scope. Each variable is "_localName = rhsExpr". Strings get an extra + // "_localName_inlineHeaderArray = __localName_headerSpan" entry. + bool stringPinnablesEmitted = false; + + if (hasAnyVoidStarPinnable) + { + writer.Write($"{indent}{new string(' ', fixedNesting * 4)}fixed(void* "); + bool first = true; + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + bool isString = p.Type.IsString(); + bool isType = p.Type.IsSystemType(); + bool isPassArray = cat is ParameterCategory.PassArray or ParameterCategory.FillArray; + + if (!isString && !isType && !isPassArray) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + + if (!first) + { + writer.Write(", "); + } + + first = false; + writer.Write($"_{localName} = "); + + if (isType) + { + writer.Write($"__{localName}"); + } + else if (isPassArray) + { + TypeSignature elemT = ((SzArrayTypeSignature)p.Type).BaseType; + bool isBlittableElem = context.AbiTypeShapeResolver.IsBlittablePrimitive(elemT) || context.AbiTypeShapeResolver.IsBlittableStruct(elemT); + bool isStringElem = elemT.IsString(); + + if (isBlittableElem) + { + writer.Write(callName); + } + else + { + writer.Write($"__{localName}_span"); + } + + // For string elements: only PassArray needs the additional inlineHeaderArray + // pinned alongside the data span. FillArray fills HSTRINGs into the nint + // storage directly (no header conversion needed). + if (isStringElem && cat == ParameterCategory.PassArray) + { + writer.Write($", _{localName}_inlineHeaderArray = __{localName}_headerSpan"); + } + } + else + { + // string param + writer.Write(callName); + } + } + writer.WriteLine($$""" + ) + {{indent}}{{new string(' ', fixedNesting * 4)}}{ + """, isMultiline: true); + fixedNesting++; + // Inside the body: emit HStringMarshaller calls for input string params. + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (!sig.Parameters[i].Type.IsString()) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(sig.Parameters[i], paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(sig.Parameters[i], paramNameOverride); + writer.WriteLine($"{indent}{new string(' ', fixedNesting * 4)}HStringMarshaller.ConvertToUnmanagedUnsafe((char*)_{localName}, {callName}?.Length, out HStringReference __{localName});"); + } + stringPinnablesEmitted = true; + } + else if (typedFixedCount > 0) + { + // Typed fixed lines exist but no void* combined block - we need a body block + // to host them. Open a brace block after the last typed fixed line. + writer.WriteLine($"{indent}{new string(' ', fixedNesting * 4)}{{"); + fixedNesting++; + } + + // Suppress unused variable warning when block above doesn't fire. + _ = stringPinnablesEmitted; + + string callIndent = indent + new string(' ', fixedNesting * 4); + + // For non-blittable PassArray params, emit CopyToUnmanaged_ (UnsafeAccessor) and call + // it to populate the inline/pooled storage from the user-supplied span. For string arrays, + // use HStringArrayMarshaller.ConvertToUnmanagedUnsafe instead. + // FillArray of strings is the exception: the native side fills the HSTRING handles, so + // there's nothing to convert pre-call (the post-call CopyToManaged_ handles writeback). + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + + if (szArr.BaseType.IsString()) + { + // Skip pre-call ConvertToUnmanagedUnsafe for FillArray of strings — there's + // nothing to convert (native fills the handles). + if (cat == ParameterCategory.FillArray) + { + continue; + } + + writer.WriteLine($$""" + {{callIndent}}HStringArrayMarshaller.ConvertToUnmanagedUnsafe( + {{callIndent}} source: {{callName}}, + {{callIndent}} hstringHeaders: (HStringHeader*) _{{localName}}_inlineHeaderArray, + {{callIndent}} hstrings: __{{localName}}_span, + {{callIndent}} pinnedGCHandles: __{{localName}}_pinnedHandleSpan); + """, isMultiline: true); + } + else + { + // FillArray (Span) of non-blittable element types: skip pre-call + // CopyToUnmanaged. The buffer the native side gets (_) is uninitialized + // ABI-format storage; the native callee fills it. The post-call writeback loop + // emits CopyToManaged_ to propagate the native fills into the user's + // managed Span. + if (cat == ParameterCategory.FillArray) + { + continue; + } + + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(szArr.BaseType)); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + // For mapped value types (DateTime/TimeSpan) and complex structs, the storage + // element is the ABI struct type; the data pointer parameter type uses that + // ABI struct. The fixed() opens with void* (per truth's pattern), so a cast + // is required at the call site. For runtime classes/objects, use void**. + string dataParamType; + string dataCastType; + + if (context.AbiTypeShapeResolver.IsMappedAbiValueType(szArr.BaseType)) + { + dataParamType = AbiTypeHelpers.GetMappedAbiTypeName(szArr.BaseType) + "*"; + dataCastType = "(" + AbiTypeHelpers.GetMappedAbiTypeName(szArr.BaseType) + "*)"; + } + else if (szArr.BaseType.IsHResultException()) + { + dataParamType = "global::ABI.System.Exception*"; + dataCastType = "(global::ABI.System.Exception*)"; + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(szArr.BaseType)) + { + string abiStructName = AbiTypeHelpers.GetAbiStructTypeName(writer, context, szArr.BaseType); + dataParamType = abiStructName + "*"; + dataCastType = "(" + abiStructName + "*)"; + } + else + { + dataParamType = "void**"; + dataCastType = "(void**)"; + } + + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "CopyToUnmanaged")] + {{callIndent}}static extern void CopyToUnmanaged_{{localName}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szArr.BaseType)}}")] object _, ReadOnlySpan<{{elementProjected}}> span, uint length, {{dataParamType}} data); + {{callIndent}}CopyToUnmanaged_{{localName}}(null, {{callName}}, (uint){{callName}}.Length, {{dataCastType}}_{{localName}}); + """, isMultiline: true); + } + } + + writer.Write(callIndent); + // method/property is [NoException] (its HRESULT is contractually S_OK). + if (!isNoExcept) + { + writer.Write("RestrictedErrorInfo.ThrowExceptionForHR((*(delegate* unmanaged[MemberFunction]<"); + } + else + { + writer.Write("(*(delegate* unmanaged[MemberFunction]<"); + } + + writer.Write($"{fp}>**)ThisPtr)[{slot.ToString(CultureInfo.InvariantCulture)}](ThisPtr"); + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + writer.Write($$""" + , + (uint){{callName}}.Length, _{{localName}} + """, isMultiline: true); + continue; + } + + if (cat == ParameterCategory.Out) + { + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + writer.Write($$""" + , + &__{{localName}} + """, isMultiline: true); + continue; + } + + if (cat == ParameterCategory.ReceiveArray) + { + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + writer.Write($$""" + , + &__{{localName}}_length, &__{{localName}}_data + """, isMultiline: true); + continue; + } + + if (cat == ParameterCategory.Ref) + { + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + TypeSignature uRefArg = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (context.AbiTypeShapeResolver.IsComplexStruct(uRefArg)) + { + // Complex struct 'in' (Ref) param: pass &__local (the marshaled ABI struct). + writer.Write($$""" + , + &__{{localName}} + """, isMultiline: true); + } + else + { + // 'in T' projected param: pass the pinned pointer. + writer.Write($$""" + , + _{{localName}} + """, isMultiline: true); + } + continue; + } + writer.Write(""" + , + + """, isMultiline: true); + if (p.Type.IsHResultException()) + { + writer.Write($"__{AbiTypeHelpers.GetParamLocalName(p, paramNameOverride)}"); + } + else if (p.Type.IsString()) + { + writer.Write($"__{AbiTypeHelpers.GetParamLocalName(p, paramNameOverride)}.HString"); + } + else if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(p.Type) || p.Type.IsObject() || p.Type.IsGenericInstance()) + { + writer.Write($"__{AbiTypeHelpers.GetParamLocalName(p, paramNameOverride)}.GetThisPtrUnsafe()"); + } + else if (p.Type.IsSystemType()) + { + // System.Type input: pass the pre-converted ABI Type struct (via the local set up before the call). + writer.Write($"__{AbiTypeHelpers.GetParamLocalName(p, paramNameOverride)}.ConvertToUnmanagedUnsafe()"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(p.Type)) + { + // Mapped value-type input: pass the pre-converted ABI local. + writer.Write($"__{AbiTypeHelpers.GetParamLocalName(p, paramNameOverride)}"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(p.Type)) + { + // Complex struct input: pass the pre-converted ABI struct local. + writer.Write($"__{AbiTypeHelpers.GetParamLocalName(p, paramNameOverride)}"); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(p.Type)) + { + writer.Write(AbiTypeHelpers.GetParamName(p, paramNameOverride)); + } + else + { + EmitParamArgConversion(writer, context, p, paramNameOverride); + } + } + + if (returnIsReceiveArray) + { + writer.Write(""" + , + &__retval_length, &__retval_data + """, isMultiline: true); + } + else if (rt is not null) + { + writer.Write(""" + , + &__retval + """, isMultiline: true); + } + + // Close the vtable call. One less ')' when noexcept (no ThrowExceptionForHR wrap). + writer.WriteLine(isNoExcept ? ");" : "));"); + + // After call: copy native-filled values back into the user's managed Span for + // FillArray of non-blittable element types. The native callee wrote into our + // ABI-format buffer (_) which is separate from the user's Span; we need to + // CopyToManaged_ to convert each ABI element back to the projected form and + // store it in the user's Span.write_marshal_from_abi + // Blittable element types (primitives and almost-blittable structs) don't need this + // because the user's Span wraps the same memory the native side wrote to. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.FillArray) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szFA) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szFA.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szFA.BaseType)) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(szFA.BaseType)); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szFA.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + // Determine the ABI element type for the data pointer parameter. + // - Strings / runtime classes / objects: void** + // - HResult exception: global::ABI.System.Exception* + // - Mapped value types: global::ABI.System.{DateTimeOffset|TimeSpan}* + // - Complex structs: * + string dataParamType; + string dataCastType; + + if (szFA.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(szFA.BaseType) || szFA.BaseType.IsObject()) + { + dataParamType = "void** data"; + dataCastType = "(void**)"; + } + else if (szFA.BaseType.IsHResultException()) + { + dataParamType = "global::ABI.System.Exception* data"; + dataCastType = "(global::ABI.System.Exception*)"; + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(szFA.BaseType)) + { + string abiName = AbiTypeHelpers.GetMappedAbiTypeName(szFA.BaseType); + dataParamType = abiName + "* data"; + dataCastType = "(" + abiName + "*)"; + } + else + { + string abiStructName = AbiTypeHelpers.GetAbiStructTypeName(writer, context, szFA.BaseType); + dataParamType = abiStructName + "* data"; + dataCastType = "(" + abiStructName + "*)"; + } + + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "CopyToManaged")] + {{callIndent}}static extern void CopyToManaged_{{localName}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szFA.BaseType)}}")] object _, uint length, {{dataParamType}}, Span<{{elementProjected}}> span); + {{callIndent}}CopyToManaged_{{localName}}(null, (uint)__{{localName}}_span.Length, {{dataCastType}}_{{localName}}, {{callName}}); + """, isMultiline: true); + } + + // After call: write back Out params to caller's 'out' var. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + TypeSignature uOut = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + // For Out generic instance: emit inline UnsafeAccessor to ConvertToManaged_ + // before the writeback. (e.g. Collection1HandlerInvoke + // emits the accessor inside try, right before the assignment). + if (uOut.IsGenericInstance()) + { + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(uOut, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, uOut, false); + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToManaged")] + {{callIndent}}static extern {{projectedTypeName}} ConvertToManaged_{{localName}}([UnsafeAccessorType("{{interopTypeName}}")] object _, void* value); + {{callIndent}}{{callName}} = ConvertToManaged_{{localName}}(null, __{{localName}}); + """, isMultiline: true); + continue; + } + + writer.Write($"{callIndent}{callName} = "); + + if (uOut.IsString()) + { + writer.Write($"HStringMarshaller.ConvertToManaged(__{localName})"); + } + else if (uOut.IsObject()) + { + writer.Write($"WindowsRuntimeObjectMarshaller.ConvertToManaged(__{localName})"); + } + else if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(uOut)) + { + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, uOut)}.ConvertToManaged(__{localName})"); + } + else if (uOut.IsSystemType()) + { + writer.Write($"global::ABI.System.TypeMarshaller.ConvertToManaged(__{localName})"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(uOut)) + { + writer.Write($"{AbiTypeHelpers.GetMarshallerFullName(writer, context, uOut)}.ConvertToManaged(__{localName})"); + } + else if (context.AbiTypeShapeResolver.IsBlittableStruct(uOut)) + { + writer.Write($"__{localName}"); + } + else if (uOut is CorLibTypeSignature corlibBool && corlibBool.ElementType == ElementType.Boolean) + { + writer.Write($"__{localName}"); + } + else if (uOut is CorLibTypeSignature corlibChar && corlibChar.ElementType == ElementType.Char) + { + writer.Write($"__{localName}"); + } + else if (context.AbiTypeShapeResolver.IsEnumType(uOut)) + { + // Enum out param: __ local is already the projected enum type (since the + // function pointer signature uses the projected type). No cast needed. + writer.Write($"__{localName}"); + } + else + { + writer.Write($"__{localName}"); + } + + writer.WriteLine(";"); + } + + // Writeback for ReceiveArray params: emit a UnsafeAccessor + assign to the out param. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.ReceiveArray) + { + continue; + } + + string callName = AbiTypeHelpers.GetParamName(p, paramNameOverride); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + SzArrayTypeSignature sza = (SzArrayTypeSignature)AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(sza.BaseType)); + // Element ABI type: void* for ref types (string/runtime class/object); ABI struct + // type for complex structs (e.g. authored BasicStruct); blittable struct ABI for + // blittable structs; primitive ABI otherwise. + string elementAbi = sza.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(sza.BaseType) || sza.BaseType.IsObject() + ? "void*" + : context.AbiTypeShapeResolver.IsComplexStruct(sza.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, sza.BaseType) + : context.AbiTypeShapeResolver.IsBlittableStruct(sza.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, sza.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, sza.BaseType); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + string marshallerPath = ArrayElementEncoder.GetArrayMarshallerInteropPath(sza.BaseType); + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToManaged")] + {{callIndent}}static extern {{elementProjected}}[] ConvertToManaged_{{localName}}([UnsafeAccessorType("{{marshallerPath}}")] object _, uint length, {{elementAbi}}* data); + {{callIndent}}{{callName}} = ConvertToManaged_{{localName}}(null, __{{localName}}_length, __{{localName}}_data); + """, isMultiline: true); + } + + if (rt is not null) + { + if (returnIsReceiveArray) + { + SzArrayTypeSignature retSz = (SzArrayTypeSignature)rt; + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(retSz.BaseType)); + string elementAbi = retSz.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSz.BaseType) || retSz.BaseType.IsObject() + ? "void*" + : context.AbiTypeShapeResolver.IsComplexStruct(retSz.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, retSz.BaseType) + : retSz.BaseType.IsHResultException() + ? "global::ABI.System.Exception" + : context.AbiTypeShapeResolver.IsMappedAbiValueType(retSz.BaseType) + ? AbiTypeHelpers.GetMappedAbiTypeName(retSz.BaseType) + : context.AbiTypeShapeResolver.IsBlittableStruct(retSz.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, retSz.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, retSz.BaseType); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(retSz.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToManaged")] + {{callIndent}}static extern {{elementProjected}}[] ConvertToManaged_retval([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(retSz.BaseType)}}")] object _, uint length, {{elementAbi}}* data); + {{callIndent}}return ConvertToManaged_retval(null, __retval_length, __retval_data); + """, isMultiline: true); + } + else if (returnIsHResultException) + { + writer.WriteLine($"{callIndent}return global::ABI.System.ExceptionMarshaller.ConvertToManaged(__retval);"); + } + else if (returnIsString) + { + writer.WriteLine($"{callIndent}return HStringMarshaller.ConvertToManaged(__retval);"); + } + else if (returnIsRefType) + { + if (rt.IsNullableT()) + { + // Nullable return: use Marshaller.UnboxToManaged.; + // there is no NullableMarshaller, the inner-T marshaller has UnboxToManaged. + TypeSignature inner = rt.GetNullableInnerType()!; + string innerMarshaller = AbiTypeHelpers.GetNullableInnerMarshallerName(writer, context, inner); + writer.WriteLine($"{callIndent}return {innerMarshaller}.UnboxToManaged(__retval);"); + } + else if (rt.IsGenericInstance()) + { + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(rt, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, rt, false); + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToManaged")] + {{callIndent}}static extern {{projectedTypeName}} ConvertToManaged_retval([UnsafeAccessorType("{{interopTypeName}}")] object _, void* value); + {{callIndent}}return ConvertToManaged_retval(null, __retval); + """, isMultiline: true); + } + else + { + writer.Write($"{callIndent}return "); + EmitMarshallerConvertToManaged(writer, context, rt, "__retval"); + writer.WriteLine(";"); + } + } + else if (rt is not null && context.AbiTypeShapeResolver.IsMappedAbiValueType(rt)) + { + // Mapped value type return (e.g. DateTime/TimeSpan): convert ABI struct back via marshaller. + writer.WriteLine($"{callIndent}return {AbiTypeHelpers.GetMappedMarshallerName(rt)}.ConvertToManaged(__retval);"); + } + else if (rt is not null && rt.IsSystemType()) + { + // System.Type return: convert ABI Type struct back to System.Type via TypeMarshaller. + writer.WriteLine($"{callIndent}return global::ABI.System.TypeMarshaller.ConvertToManaged(__retval);"); + } + else if (returnIsBlittableStruct) + { + writer.Write(callIndent); + + if (rt is not null && context.AbiTypeShapeResolver.IsMappedAbiValueType(rt)) + { + // Mapped value type return: convert ABI struct back to projected via marshaller. + writer.WriteLine($"return {AbiTypeHelpers.GetMappedMarshallerName(rt)}.ConvertToManaged(__retval);"); + } + else + { + writer.WriteLine("return __retval;"); + } + } + else if (returnIsComplexStruct) + { + writer.WriteLine($"{callIndent}return {AbiTypeHelpers.GetMarshallerFullName(writer, context, rt!)}.ConvertToManaged(__retval);"); + } + else + { + writer.Write($"{callIndent}return "); + string projected = MethodFactory.WriteProjectedSignature(context, rt!, false); + string abiType = AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, rt!); + + if (projected == abiType) + { + writer.WriteLine("__retval;"); + } + else + { + writer.WriteLine($"({projected})__retval;"); + } + } + } + + // Close fixed blocks (innermost first). + for (int i = fixedNesting - 1; i >= 0; i--) + { + writer.WriteLine($"{indent}{new string(' ', i * 4)}}}"); + } + + if (needsTryFinally) + { + writer.WriteLine(""" + } + finally + { + """, isMultiline: true); + + // Order matches truth: + // 0. Complex-struct input param Dispose (e.g. ProfileUsageMarshaller.Dispose(__value)) + // 1. Non-blittable PassArray/FillArray cleanup (Dispose + ArrayPools) + // 2. Out param frees (HString / object / runtime class) + // 3. ReceiveArray param frees (Free_ via UnsafeAccessor) + // 4. Return free (__retval) — last + + // 0. Dispose complex-struct input params via marshaller (both 'in' and 'in T' forms). + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.In or ParameterCategory.Ref)) + { + continue; + } + + TypeSignature pType = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + + if (!context.AbiTypeShapeResolver.IsComplexStruct(pType)) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + writer.WriteLine($" {AbiTypeHelpers.GetMarshallerFullName(writer, context, pType)}.Dispose(__{localName});"); + } + + // 1. Cleanup non-blittable PassArray/FillArray params: + // For strings: HStringArrayMarshaller.Dispose + return ArrayPools (3 of them). + // For runtime classes/objects: Dispose_ (UnsafeAccessor) + return ArrayPool. + // For mapped value types (DateTime/TimeSpan): no per-element disposal needed and truth + // doesn't return the ArrayPool either, so skip entirely. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsMappedAbiValueType(szArr.BaseType)) + { + continue; + } + + if (szArr.BaseType.IsHResultException()) + { + // HResultException ABI is just an int; per-element Dispose is a no-op (mirror + // the truth: no Dispose_ emitted). Just return the inline-array's pool + // using the correct element type (ABI.System.Exception, not nint). + string localNameH = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + writer.WriteLine(); + writer.WriteLine($$""" + if (__{{localNameH}}_arrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{localNameH}}_arrayFromPool); + } + """, isMultiline: true); + continue; + } + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + + if (szArr.BaseType.IsString()) + { + // The HStringArrayMarshaller.Dispose + ArrayPool returns for strings only + // apply to PassArray (where we set up the pinned handles + headers in the + // first place). FillArray writes back HSTRING handles into the nint storage + // array directly, with no per-element pinned handle / header to release. + if (cat == ParameterCategory.PassArray) + { + writer.WriteLine($$""" + HStringArrayMarshaller.Dispose(__{{localName}}_pinnedHandleSpan); + + if (__{{localName}}_pinnedHandleArrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{localName}}_pinnedHandleArrayFromPool); + } + + if (__{{localName}}_headerArrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{localName}}_headerArrayFromPool); + } + """, isMultiline: true); + } + + // Both PassArray and FillArray need the inline-array's nint pool returned. + writer.WriteLine(); + writer.WriteLine($$""" + if (__{{localName}}_arrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{localName}}_arrayFromPool); + } + """, isMultiline: true); + } + else + { + // For complex structs, both the Dispose_ data param and the fixed() + // pointer must be typed as *; the cast can be omitted. For + // runtime classes / objects / strings the data is void** and the fixed() + // remains void* with a (void**) cast. + string disposeDataParamType; + string fixedPtrType; + string disposeCastType; + + if (context.AbiTypeShapeResolver.IsComplexStruct(szArr.BaseType)) + { + string abiStructName = AbiTypeHelpers.GetAbiStructTypeName(writer, context, szArr.BaseType); + disposeDataParamType = abiStructName + "*"; + fixedPtrType = abiStructName + "*"; + disposeCastType = string.Empty; + } + else + { + disposeDataParamType = "void** data"; + fixedPtrType = "void*"; + disposeCastType = "(void**)"; + } + + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Dispose")] + static extern void Dispose_{{localName}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szArr.BaseType)}}")] object _, uint length, {{disposeDataParamType}} + """, isMultiline: true); + if (!disposeDataParamType.EndsWith("data", StringComparison.Ordinal)) + { + writer.Write(" data"); + } + + writer.WriteLine($$""" + ); + + fixed({{fixedPtrType}} _{{localName}} = __{{localName}}_span) + { + Dispose_{{localName}}(null, (uint) __{{localName}}_span.Length, {{disposeCastType}}_{{localName}}); + } + """, isMultiline: true); + } + + // ArrayPool storage type matches the InlineArray storage (mapped ABI value type + // for DateTime/TimeSpan; ABI struct for complex structs; nint otherwise). + string poolStorageT = context.AbiTypeShapeResolver.IsMappedAbiValueType(szArr.BaseType) + ? AbiTypeHelpers.GetMappedAbiTypeName(szArr.BaseType) + : context.AbiTypeShapeResolver.IsComplexStruct(szArr.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, szArr.BaseType) + : "nint"; + writer.WriteLine(); + writer.WriteLine($$""" + if (__{{localName}}_arrayFromPool is not null) + { + global::System.Buffers.ArrayPool<{{poolStorageT}}>.Shared.Return(__{{localName}}_arrayFromPool); + } + """, isMultiline: true); + } + + // 2. Free Out string/object/runtime-class params. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.Out) + { + continue; + } + + TypeSignature uOut = AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + + if (uOut.IsString()) + { + writer.WriteLine($" HStringMarshaller.Free(__{localName});"); + } + else if (uOut.IsObject() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(uOut) || uOut.IsGenericInstance()) + { + writer.WriteLine($" WindowsRuntimeUnknownMarshaller.Free(__{localName});"); + } + else if (uOut.IsSystemType()) + { + writer.WriteLine($" global::ABI.System.TypeMarshaller.Dispose(__{localName});"); + } + else if (context.AbiTypeShapeResolver.IsComplexStruct(uOut)) + { + writer.WriteLine($" {AbiTypeHelpers.GetMarshallerFullName(writer, context, uOut)}.Dispose(__{localName});"); + } + } + + // 3. Free ReceiveArray params via UnsafeAccessor. + for (int i = 0; i < sig.Parameters.Count; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat != ParameterCategory.ReceiveArray) + { + continue; + } + + string localName = AbiTypeHelpers.GetParamLocalName(p, paramNameOverride); + SzArrayTypeSignature sza = (SzArrayTypeSignature)AbiTypeHelpers.StripByRefAndCustomModifiers(p.Type); + // Element ABI type: void* for ref types; ABI struct for complex/blittable structs; + // primitive ABI otherwise. (Same categorization as the ConvertToManaged_ path.) + string elementAbi = sza.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(sza.BaseType) || sza.BaseType.IsObject() + ? "void*" + : context.AbiTypeShapeResolver.IsComplexStruct(sza.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, sza.BaseType) + : context.AbiTypeShapeResolver.IsBlittableStruct(sza.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, sza.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, sza.BaseType); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(sza.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + string marshallerPath = ArrayElementEncoder.GetArrayMarshallerInteropPath(sza.BaseType); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Free")] + static extern void Free_{{localName}}([UnsafeAccessorType("{{marshallerPath}}")] object _, uint length, {{elementAbi}}* data); + + Free_{{localName}}(null, __{{localName}}_length, __{{localName}}_data); + """, isMultiline: true); + } + + // 4. Free return value (__retval) — emitted last to match truth ordering. + if (returnIsString) + { + writer.WriteLine(" HStringMarshaller.Free(__retval);"); + } + else if (returnIsRefType) + { + writer.WriteLine(" WindowsRuntimeUnknownMarshaller.Free(__retval);"); + } + else if (returnIsComplexStruct) + { + writer.WriteLine($" {AbiTypeHelpers.GetMarshallerFullName(writer, context, rt!)}.Dispose(__retval);"); + } + else if (returnIsSystemTypeForCleanup) + { + // System.Type return: dispose the ABI.System.Type's HSTRING fields. + writer.WriteLine(" global::ABI.System.TypeMarshaller.Dispose(__retval);"); + } + else if (returnIsReceiveArray) + { + SzArrayTypeSignature retSz = (SzArrayTypeSignature)rt!; + string elementAbi = retSz.BaseType.IsString() || context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(retSz.BaseType) || retSz.BaseType.IsObject() + ? "void*" + : context.AbiTypeShapeResolver.IsComplexStruct(retSz.BaseType) + ? AbiTypeHelpers.GetAbiStructTypeName(writer, context, retSz.BaseType) + : retSz.BaseType.IsHResultException() + ? "global::ABI.System.Exception" + : context.AbiTypeShapeResolver.IsMappedAbiValueType(retSz.BaseType) + ? AbiTypeHelpers.GetMappedAbiTypeName(retSz.BaseType) + : context.AbiTypeShapeResolver.IsBlittableStruct(retSz.BaseType) + ? AbiTypeHelpers.GetBlittableStructAbiType(writer, context, retSz.BaseType) + : AbiTypeHelpers.GetAbiPrimitiveType(context.Cache, retSz.BaseType); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(retSz.BaseType, TypedefNameType.Projected); + + _ = elementInteropArg; + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Free")] + static extern void Free_retval([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(retSz.BaseType)}}")] object _, uint length, {{elementAbi}}* data); + Free_retval(null, __retval_length, __retval_data); + """, isMultiline: true); + } + + writer.WriteLine(" }"); + } + + writer.WriteLine(" }"); + } + + /// + /// Emits the conversion of a parameter from its projected (managed) form to the ABI argument form. + /// + internal static void EmitParamArgConversion(IndentedTextWriter writer, ProjectionEmitContext context, ParameterInfo p, string? paramNameOverride = null) + { + string pname = paramNameOverride ?? p.Parameter.Name ?? "param"; + // bool: ABI is 'bool' directly; pass as-is. + if (p.Type is CorLibTypeSignature corlib && + corlib.ElementType == ElementType.Boolean) + { + writer.Write(pname); + } + + // char: ABI is 'char' directly; pass as-is. + else if (p.Type is CorLibTypeSignature corlib2 && + corlib2.ElementType == ElementType.Char) + { + writer.Write(pname); + } + + // Enums: function pointer signature uses the projected enum type, so pass directly. + else if (context.AbiTypeShapeResolver.IsEnumType(p.Type)) + { + writer.Write(pname); + } + else + { + writer.Write(pname); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs new file mode 100644 index 0000000000..a5e2d56e2d --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the ABI method body shapes for runtime interface vtable invocations: simple/forwarding +/// bodies, parameter conversion glue, and per-method UnsafeAccessor accessors for generic vtables. +/// +/// +/// The implementation is split across several partial files: +/// +/// AbiMethodBodyFactory.DoAbi.cs - CCW Do_Abi_* method body emission. +/// AbiMethodBodyFactory.RcwCaller.cs - RCW caller (instance-method) body emission. +/// AbiMethodBodyFactory.MethodsClass.cs - The static *Methods class members (caller dispatch hub). +/// AbiMethodBodyFactory.MarshallerDispatch.cs - Per-marshaller ConvertToManaged/Unmanaged dispatch helpers. +/// +/// +internal static partial class AbiMethodBodyFactory +{ +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Factories/AbiStructFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiStructFactory.cs new file mode 100644 index 0000000000..972f03e4f0 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/AbiStructFactory.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the ABI struct layout (when needed), the ABI marshaller class, and the +/// IReference<T> impl for a projected struct type. +/// +internal static class AbiStructFactory +{ + /// + /// Writes the ABI struct, marshaller class, and IReference impl for a struct type. + /// + /// The writer to emit to. + /// The active emit context. + /// The struct type definition. + public static void WriteAbiStruct(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // 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 = AbiTypeHelpers.IsTypeBlittable(context.Cache, type); + (string typeNs, string typeNm) = type.Names(); + bool isMappedStruct = MappedTypes.Get(typeNs, typeNm) is not null; + + if (!blittable && !isMappedStruct) + { + // In component mode emit the [WindowsRuntimeMetadataTypeName]/[WindowsRuntimeMappedType] + // attribute pair; otherwise emit the [ComWrappersMarshaller] attribute. Both branches + // then emit [WindowsRuntimeClassName] + the struct definition with public ABI fields. + if (context.Settings.Component) + { + MetadataAttributeFactory.WriteWinRTMetadataTypeNameAttribute(writer, context, type); + MetadataAttributeFactory.WriteWinRTMappedTypeAttribute(writer, context, type); + } + else + { + MetadataAttributeFactory.WriteComWrapperMarshallerAttribute(writer, context, type); + } + + MetadataAttributeFactory.WriteValueTypeWinRTClassNameAttribute(writer, context, type); + writer.Write($"{context.Settings.InternalAccessibility} unsafe struct "); + writer.Write(IdentifierEscaping.StripBackticks(type.Name?.Value ?? string.Empty)); + writer.WriteLine(); + using (writer.WriteBlock()) + { + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + TypeSignature ft = field.Signature.FieldType; + writer.Write("public "); + // Truth uses void* for string and Nullable fields, the ABI type 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 (ft.IsString() || AbiTypeHelpers.TryGetNullablePrimitiveMarshallerName(ft, out _)) + { + writer.Write("void*"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(ft)) + { + writer.Write(AbiTypeHelpers.GetMappedAbiTypeName(ft)); + } + else if (ft is TypeDefOrRefSignature tdr + && AbiTypeHelpers.TryResolveStructTypeDef(context.Cache, tdr) is TypeDefinition fieldTd + && TypeCategorization.GetCategory(fieldTd) == TypeCategory.Struct + && !AbiTypeHelpers.IsTypeBlittable(context.Cache, fieldTd)) + { + TypedefNameWriter.WriteTypedefName(writer, context, fieldTd, TypedefNameType.ABI, false); + } + else + { + MethodFactory.WriteProjectedSignature(writer, context, ft, false); + } + + writer.WriteLine($" {field.Name?.Value ?? string.Empty};"); + } + } + writer.WriteLine(); + } + else if (blittable && context.Settings.Component) + { + // For blittable component structs, emit the authoring metadata wrapper + // (a 'file static class T {}' with the WinRT metadata attributes). + AbiClassFactory.WriteAuthoringMetadataType(writer, context, type); + } + + StructEnumMarshallerFactory.WriteStructEnumMarshallerClass(writer, context, type); + ReferenceImplFactory.WriteReferenceImpl(writer, context, type); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs new file mode 100644 index 0000000000..713c0fe153 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs @@ -0,0 +1,793 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the projected runtime class type and its members (constructors, properties, +/// methods, events, and the activation-factory glue), plus the static-only variant +/// for [Static] classes. +/// +internal static class ClassFactory +{ + /// + /// Returns whether is marked with the [FastAbi] attribute, + /// indicating that fast-ABI calling conventions apply to its members. + /// + /// The runtime class definition to inspect. + /// if is decorated with [FastAbi]. + public static bool IsFastAbiClass(TypeDefinition type) + { + // Fast ABI is enabled when the type is marked [FastAbi]. (CsWinRT 3.0 has no + // netstandard_compat gate -- it was always false in the C# port.) + return type.HasAttribute(WindowsFoundationMetadata, FastAbiAttribute); + } + + /// + /// Writes the class modifiers ('static '/'sealed '). + /// + public static void WriteClassModifiers(IndentedTextWriter writer, TypeDefinition type) + { + if (TypeCategorization.IsStatic(type)) + { + writer.Write("static "); + return; + } + + if (type.IsSealed) + { + writer.Write("sealed "); + } + } + + /// + /// Returns the fast-abi class type for if the interface is + /// exclusive_to a class marked [FastAbi]; otherwise null. + /// + public static TypeDefinition? FindFastAbiClassType(MetadataCache cache, TypeDefinition iface) + { + TypeDefinition? exclusiveToClass = AbiTypeHelpers.GetExclusiveToType(cache, iface); + + if (exclusiveToClass is null) + { + return null; + } + + if (!IsFastAbiClass(exclusiveToClass)) + { + return null; + } + + return exclusiveToClass; + } + + /// + /// Returns the fast-abi class info (class type + default interface + sorted other exclusive + /// interfaces) for , if the interface is exclusive_to a fast-abi + /// class; otherwise null. + /// + public static (TypeDefinition Class, TypeDefinition? Default, System.Collections.Generic.List Others)? GetFastAbiClassForInterface(MetadataCache cache, TypeDefinition iface) + { + TypeDefinition? cls = FindFastAbiClassType(cache, iface); + + if (cls is null) + { + return null; + } + + (TypeDefinition? def, System.Collections.Generic.List others) = GetFastAbiInterfaces(cache, cls); + return (cls, def, others); + } + + /// + /// Whether is a non-default exclusive interface of a fast-abi class + /// (i.e. its members are merged into the default interface's vtable and dispatched through + /// the default interface's ABI Methods class). + /// + public static bool IsFastAbiOtherInterface(MetadataCache cache, TypeDefinition iface) + { + (TypeDefinition Class, TypeDefinition? Default, List Others)? fastAbi = GetFastAbiClassForInterface(cache, iface); + + if (fastAbi is null) + { + return false; + } + + if (fastAbi.Value.Default is not null && InterfacesEqual(fastAbi.Value.Default, iface)) + { + return false; + } + + foreach (TypeDefinition other in fastAbi.Value.Others) + { + if (InterfacesEqual(other, iface)) + { + return true; + } + } + return false; + } + + /// + /// Returns true if is the default interface of a fast-abi class. + /// + public static bool IsFastAbiDefaultInterface(MetadataCache cache, TypeDefinition iface) + { + (TypeDefinition Class, TypeDefinition? Default, List Others)? fastAbi = GetFastAbiClassForInterface(cache, iface); + + if (fastAbi is null) + { + return false; + } + + return fastAbi.Value.Default is not null && InterfacesEqual(fastAbi.Value.Default, iface); + } + + private static bool InterfacesEqual(TypeDefinition a, TypeDefinition b) + { + if (a == b) + { + return true; + } + + return (a.Namespace?.Value ?? string.Empty) == (b.Namespace?.Value ?? string.Empty) + && (a.Name?.Value ?? string.Empty) == (b.Name?.Value ?? string.Empty); + } + + // We don't have direct access to the active Settings from a static helper that only takes + // a TypeDefinition. The fast-abi flag is purely determined by the [FastAbiAttribute] (the + // netstandard_compat gate is always false in CsWinRT 3.0 -- the flag has been removed). + + /// + /// Returns the [Default] interface and the [ExclusiveTo] interfaces (sorted) for fast ABI. + /// + public static (TypeDefinition? DefaultInterface, System.Collections.Generic.List OtherInterfaces) GetFastAbiInterfaces(MetadataCache cache, TypeDefinition classType) + { + TypeDefinition? defaultIface = null; + System.Collections.Generic.List exclusiveIfaces = []; + foreach (InterfaceImplementation impl in classType.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + TypeDefinition? ifaceTd = impl.Interface as TypeDefinition + ?? impl.Interface.TryResolve(cache.RuntimeContext); + + if (ifaceTd is null) + { + continue; + } + + if (impl.IsDefaultInterface()) + { + defaultIface = ifaceTd; + } + else if (TypeCategorization.IsExclusiveTo(ifaceTd)) + { + exclusiveIfaces.Add(ifaceTd); + } + } + + // Sort exclusive interfaces by: + // 1. Number of [PreviousContractVersion] attrs (ascending; newer interfaces have more) + // 2. Contract version (ascending) + // 3. Type version (ascending) + // 4. Type namespace and name (ascending) + exclusiveIfaces.Sort((a, b) => + { + int aPrev = -CountAttributes(a, WindowsFoundationMetadata, "PreviousContractVersionAttribute"); + int bPrev = -CountAttributes(b, WindowsFoundationMetadata, "PreviousContractVersionAttribute"); + + if (aPrev != bPrev) + { + return aPrev.CompareTo(bPrev); + } + + int? aCV = a.GetContractVersion(); + int? bCV = b.GetContractVersion(); + + if (aCV.HasValue && bCV.HasValue && aCV.Value != bCV.Value) + { + return aCV.Value.CompareTo(bCV.Value); + } + + int? aV = a.GetVersion(); + int? bV = b.GetVersion(); + + if (aV.HasValue && bV.HasValue && aV.Value != bV.Value) + { + return aV.Value.CompareTo(bV.Value); + } + + string aNs = a.Namespace?.Value ?? string.Empty; + string bNs = b.Namespace?.Value ?? string.Empty; + + if (aNs != bNs) + { + return StringComparer.Ordinal.Compare(aNs, bNs); + } + + return StringComparer.Ordinal.Compare(a.Name?.Value ?? string.Empty, b.Name?.Value ?? string.Empty); + }); + return (defaultIface, exclusiveIfaces); + } + + private static int CountAttributes(IHasCustomAttribute member, string ns, string name) + { + int count = 0; + for (int i = 0; i < member.CustomAttributes.Count; i++) + { + CustomAttribute attr = member.CustomAttributes[i]; + ITypeDefOrRef? type = attr.Constructor?.DeclaringType; + + if (type is not null && type.Namespace == ns && type.Name == name) + { + count++; + } + } + return count; + } + /// + /// Returns the GC-pressure cost (in bytes) declared by [GCPressureAttribute] on , or 0 if not annotated. + /// + public static int GetGcPressureAmount(TypeDefinition type) + { + if (!type.IsSealed) + { + return 0; + } + + CustomAttribute? attr = type.GetAttribute(WindowsFoundationMetadata, "GCPressureAttribute"); + + if (attr is null || attr.Signature is null) + { + return 0; + } + + // The attribute has a single named arg "Amount" of an enum type. Defaults: 0=Low, 1=Medium, 2=High. + // We try both fixed args and named args. + int amount = -1; + + if (attr.Signature.NamedArguments.Count > 0) + { + object? v = attr.Signature.NamedArguments[0].Argument.Element; + + if (v is int i) + { + amount = i; + } + } + + return amount switch + { + 0 => 12000, + 1 => 120000, + 2 => 1200000, + _ => 0 + }; + } + + /// + /// Writes a static class declaration with [ContractVersion]-derived platform suppression. + /// + public static void WriteStaticClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + using (context.EnterPlatformSuppressionScope(string.Empty)) + { + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, true); + writer.Write($"{context.Settings.InternalAccessibility} static class "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine(); + using (writer.WriteBlock()) + { + WriteStaticClassMembers(writer, context, type); + } + } + } + + /// + /// Emits static members from [Static] factory interfaces. + /// + public static void WriteStaticClassMembers(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Cache is null) + { + return; + } + + // Per-property accessor state (origin tracking for getter/setter) + Dictionary properties = []; + // Track the static factory ifaces we've emitted objref fields for (to dedupe) + HashSet emittedObjRefs = []; + + string runtimeClassFullName = (type.Namespace?.Value ?? string.Empty) + "." + (type.Name?.Value ?? string.Empty); + + foreach (KeyValuePair kv in AttributedTypes.Get(type, context.Cache)) + { + AttributedType factory = kv.Value; + + if (!(factory.Statics && factory.Type is not null)) + { + continue; + } + + TypeDefinition staticIface = factory.Type; + + // Compute the objref name for this static factory interface. + string objRef = ObjRefNameGenerator.GetObjRefName(context, staticIface); + // Compute the ABI Methods static class name (e.g. "global::ABI.Windows.System.ILauncherStaticsMethods") + string abiClass = TypedefNameWriter.WriteTypedefName(context, staticIface, TypedefNameType.StaticAbiClass, true); + + if (!abiClass.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + abiClass = GlobalPrefix + abiClass; + } + + // Emit the lazy static objref field (mirrors truth's pattern) once per static iface. + if (emittedObjRefs.Add(objRef)) + { + WriteStaticFactoryObjRef(writer, context, staticIface, runtimeClassFullName, objRef); + } + + // Compute the platform attribute string from the static factory interface's + // [ContractVersion] attribute + string platformAttribute = CustomAttributeFactory.WritePlatformAttribute(context, staticIface); + + // Methods + foreach (MethodDefinition method in staticIface.Methods) + { + if (method.IsSpecial()) + { + continue; + } + + MethodSignatureInfo sig = new(method); + string mname = method.Name?.Value ?? string.Empty; + writer.WriteLine(); + + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write("public static "); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {mname}("); + MethodFactory.WriteParameterList(writer, context, sig); + + if (context.Settings.ReferenceProjection) + { + // method bodies become 'throw null' in reference projection mode. + writer.WriteLine(") => throw null;"); + } + else + { + writer.Write($") => {abiClass}.{mname}({objRef}"); + for (int i = 0; i < sig.Parameters.Count; i++) + { + writer.Write(", "); + ClassMembersFactory.WriteParameterNameWithModifier(writer, context, sig.Parameters[i]); + } + writer.WriteLine(");"); + } + } + + // Events: dispatch via static ABI class which returns an event source. + foreach (EventDefinition evt in staticIface.Events) + { + string evtName = evt.Name?.Value ?? string.Empty; + writer.WriteLine(); + + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write("public static event "); + TypedefNameWriter.WriteEventType(writer, context, evt); + writer.WriteLine($$""" + {{evtName}} + { + """, isMultiline: true); + if (context.Settings.ReferenceProjection) + { + // event accessor bodies become 'throw null' in reference projection mode. + writer.WriteLine(""" + add => throw null; + remove => throw null; + """, isMultiline: true); + } + else + { + writer.WriteLine($$""" + add => {{abiClass}}.{{evtName}}({{objRef}}, {{objRef}}).Subscribe(value); + remove => {{abiClass}}.{{evtName}}({{objRef}}, {{objRef}}).Unsubscribe(value); + """, isMultiline: true); + } + writer.WriteLine("}"); + } + + // Properties (merge getter/setter across interfaces, tracking origin per accessor) + foreach (PropertyDefinition prop in staticIface.Properties) + { + string propName = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = prop.GetPropertyMethods(); + string propType = InterfaceFactory.WritePropType(context, prop); + + if (!properties.TryGetValue(propName, out StaticPropertyAccessorState? state)) + { + state = new StaticPropertyAccessorState + { + PropTypeText = propType, + }; + properties[propName] = state; + } + + if (getter is not null && !state.HasGetter) + { + state.HasGetter = true; + state.GetterAbiClass = abiClass; + state.GetterObjRef = objRef; + state.GetterPlatformAttribute = platformAttribute; + } + + if (setter is not null && !state.HasSetter) + { + state.HasSetter = true; + state.SetterAbiClass = abiClass; + state.SetterObjRef = objRef; + state.SetterPlatformAttribute = platformAttribute; + } + } + } + + // Emit properties with merged accessors + foreach (KeyValuePair kv in properties) + { + StaticPropertyAccessorState s = kv.Value; + writer.WriteLine(); + // when getter and setter platforms match; otherwise emit per-accessor. + string getterPlat = s.GetterPlatformAttribute; + string setterPlat = s.SetterPlatformAttribute; + string propertyPlat = string.Empty; + bool bothSidesPresent = s.HasGetter && s.HasSetter; + + if (!bothSidesPresent || getterPlat == setterPlat) + { + propertyPlat = !string.IsNullOrEmpty(getterPlat) ? getterPlat : setterPlat; + getterPlat = string.Empty; + setterPlat = string.Empty; + } + + if (!string.IsNullOrEmpty(propertyPlat)) + { + writer.Write(propertyPlat); + } + + writer.Write($"public static {s.PropTypeText} {kv.Key}"); + // Getter-only -> expression body; otherwise -> accessor block (matches truth). + // In ref mode, all accessor bodies emit '=> throw null;' + bool getterOnly = s.HasGetter && !s.HasSetter; + + if (getterOnly) + { + if (context.Settings.ReferenceProjection) + { + writer.WriteLine(" => throw null;"); + } + else + { + writer.WriteLine($" => {s.GetterAbiClass}.{kv.Key}({s.GetterObjRef});"); + } + } + else + { + writer.WriteLine(); + using (writer.WriteBlock()) + { + if (s.HasGetter) + { + if (!string.IsNullOrEmpty(getterPlat)) + { + writer.Write(getterPlat); + } + + if (context.Settings.ReferenceProjection) + { + writer.WriteLine("get => throw null;"); + } + else + { + writer.WriteLine($"get => {s.GetterAbiClass}.{kv.Key}({s.GetterObjRef});"); + } + } + + if (s.HasSetter) + { + if (!string.IsNullOrEmpty(setterPlat)) + { + writer.Write(setterPlat); + } + + if (context.Settings.ReferenceProjection) + { + writer.WriteLine("set => throw null;"); + } + else + { + writer.WriteLine($"set => {s.SetterAbiClass}.{kv.Key}({s.SetterObjRef}, value);"); + } + } + } + } + } + } + + /// + /// Emits the static lazy objref property for a static factory interface (mirrors truth's + /// pattern: lazy WindowsRuntimeObjectReference.GetActivationFactory(...)). + /// + internal static void WriteStaticFactoryObjRef(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition staticIface, string runtimeClassFullName, string objRefName) + { + writer.WriteLine(); + writer.WriteLine($$""" + private static WindowsRuntimeObjectReference {{objRefName}} + { + """, isMultiline: true); + if (context.Settings.ReferenceProjection) + { + // the static factory objref getter body is just 'throw null;'. + writer.WriteLine(""" + get + { + throw null; + } + } + """, isMultiline: true); + return; + } + writer.Write($$""" + get + { + var __{{objRefName}} = field; + if (__{{objRefName}} != null && __{{objRefName}}.IsInCurrentContext) + { + return __{{objRefName}}; + } + return field = WindowsRuntimeObjectReference.GetActivationFactory("{{runtimeClassFullName}}", + """, isMultiline: true); + ObjRefNameGenerator.WriteIidExpression(writer, context, staticIface); + writer.WriteLine(""" + ); + } + } + """, isMultiline: true); + } + + /// + /// Writes a projected runtime class. + /// + public static void WriteClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.Component) + { + return; + } + + if (TypeCategorization.IsStatic(type)) + { + WriteStaticClass(writer, context, type); + return; + } + + // Tracks the highest platform seen within this class to suppress redundant + // [SupportedOSPlatform(...)] emissions across interface boundaries. + using (context.EnterPlatformSuppressionScope(string.Empty)) + { + WriteClassCore(writer, context, type); + } + } + + private static void WriteClassCore(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string typeName = type.Name?.Value ?? string.Empty; + int gcPressure = GetGcPressureAmount(type); + + // Header attributes + writer.WriteLine(); + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, true); + MetadataAttributeFactory.WriteComWrapperMarshallerAttribute(writer, context, type); + writer.Write($"{(context.Settings.Internal ? "internal" : "public")} "); + WriteClassModifiers(writer, type); + // are emitted as plain (non-partial) classes. + writer.Write("class "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + InterfaceFactory.WriteTypeInheritance(writer, context, type, false, true); + writer.WriteLine(); + using IndentedTextWriter.Block __classBlock = writer.WriteBlock(); + + // ObjRef field definitions for each implemented interface. + // These back the per-interface dispatch in instance methods/properties and the + // IWindowsRuntimeInterface.GetInterface() implementations. + ObjRefNameGenerator.WriteClassObjRefDefinitions(writer, context, type); + + // Constructor: WindowsRuntimeObjectReference-based constructor (RCW-like) + if (!context.Settings.ReferenceProjection) + { + string ctorAccess = type.IsSealed ? "internal" : "protected internal"; + writer.WriteLine(); + writer.WriteLine($$""" + {{ctorAccess}} {{typeName}}(WindowsRuntimeObjectReference nativeObjectReference) + : base(nativeObjectReference) + """, isMultiline: true); + using (writer.WriteBlock()) + { + if (!type.IsSealed) + { + // For unsealed classes, the default interface objref needs to be initialized only + // when GetType() matches the projected class exactly (derived classes have their own + // default interface). The init; accessor on _objRef_ allows this set. + ITypeDefOrRef? defaultIface = type.GetDefaultInterface(); + + if (defaultIface is not null) + { + string defaultObjRefName = ObjRefNameGenerator.GetObjRefName(context, defaultIface); + writer.WriteLine($$""" + if (GetType() == typeof({{typeName}})) + { + {{defaultObjRefName}} = NativeObjectReference; + } + """, isMultiline: true); + } + } + + if (gcPressure > 0) + { + writer.WriteLine($"GC.AddMemoryPressure({gcPressure.ToString(CultureInfo.InvariantCulture)});"); + } + } + } + else if (context.Cache is not null) + { + // In ref mode, if WriteAttributedTypes will not emit any public constructors, + // we need a 'private TypeName() { throw null; }' to suppress the C# compiler's + // implicit public default constructor (which would expose an unintended API). + // either: + // - factory.activatable is true (parameterless or parameterized — Activatable + // always emits at least one ctor), OR + // - factory.composable && factory.type && factory.type.MethodList().size() > 0 + // (composable factories with NO methods don't emit any ctors). + bool hasRefModeCtors = false; + foreach (KeyValuePair kv in AttributedTypes.Get(type, context.Cache)) + { + AttributedType factory = kv.Value; + + if (factory.Activatable) + { + hasRefModeCtors = true; + break; + } + + if (factory.Composable && factory.Type is not null && factory.Type.Methods.Count > 0) + { + hasRefModeCtors = true; + break; + } + } + + if (!hasRefModeCtors) + { + RefModeStubFactory.EmitSyntheticPrivateCtor(writer, typeName); + } + } + + // Activator/composer constructors from [Activatable]/[Composable] factory interfaces. + // write_static_members) BEFORE the override hooks and instance members. + ConstructorFactory.WriteAttributedTypes(writer, context, type); + + // Static members from [Static] factory interfaces (e.g. GetForCurrentView), emitted + // right after the attributed types to preserve the overall ordering. + WriteStaticClassMembers(writer, context, type); + + // Conditional finalizer + if (gcPressure > 0) + { + writer.WriteLine($$""" + ~{{typeName}}() + { + GC.RemoveMemoryPressure({{gcPressure.ToString(CultureInfo.InvariantCulture)}}); + } + """, isMultiline: true); + } + + // Class members from interfaces (instance methods, properties, events). + // Override hooks (HasUnwrappableNativeObjectReference and IsOverridableInterface) must + // be emitted BEFORE the public members. + if (!context.Settings.ReferenceProjection) + { + writer.WriteLine(); + writer.Write("protected override bool HasUnwrappableNativeObjectReference => "); + + if (!type.IsSealed) + { + writer.Write($"GetType() == typeof({typeName});"); + } + else + { + writer.Write("true;"); + } + + writer.WriteLine(); + writer.WriteLine(); + writer.Write("protected override bool IsOverridableInterface(in Guid iid) => "); + bool firstClause = true; + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (!impl.IsOverridable()) + { + continue; + } + + ITypeDefOrRef? implRef = impl.Interface; + + if (implRef is null) + { + continue; + } + + if (!firstClause) + { + writer.Write(" || "); + } + + firstClause = false; + ObjRefNameGenerator.WriteIidExpression(writer, context, implRef); + writer.Write(" == iid"); + } + + // base call when type has a non-object base class + bool hasBaseClass = type.BaseType is not null + && !(type.BaseType.Namespace?.Value == "System" && type.BaseType.Name?.Value == "Object") + && !(type.BaseType.Namespace?.Value == "WindowsRuntime" && type.BaseType.Name?.Value == "WindowsRuntimeObject"); + + if (hasBaseClass) + { + if (!firstClause) + { + writer.Write(" || "); + } + + writer.Write("base.IsOverridableInterface(in iid)"); + firstClause = false; + } + + if (firstClause) + { + writer.Write("false"); + } + + writer.WriteLine(";"); + } + + ClassMembersFactory.WriteClassMembers(writer, context, type); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs new file mode 100644 index 0000000000..fac644c021 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class ClassMembersFactory +{ + /// + /// Emits all instance members (methods, properties, events) inherited from implemented interfaces. + /// In reference-projection mode, type declarations and per-interface objref getters are + /// emitted, but non-mapped instance method/property/event bodies are emitted as => throw null; stubs. + /// + public static void WriteClassMembers(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + HashSet writtenMethods = []; + // For properties: track per-name accessor presence so we can merge get/set across interfaces. + // Use insertion-order Dictionary so the per-class property emission order matches the + // .winmd metadata definition order order). + Dictionary propertyState = []; + HashSet writtenEvents = []; + HashSet writtenInterfaces = []; + // interface inside WriteInterfaceMembersRecursive (right before that interface's + // members), instead of one upfront block. This interleaves the GetInterface() impls + // with their corresponding interface body, matching truth's per-interface layout. + WriteInterfaceMembersRecursive(writer, context, type, type, null, writtenMethods, propertyState, writtenEvents, writtenInterfaces); + + // After collecting all properties (with merged accessors), emit them. + foreach (KeyValuePair kvp in propertyState) + { + PropertyAccessorState s = kvp.Value; + // For generic-interface properties, emit the UnsafeAccessor static externs above the + // property declaration. Note: getter and setter use the same accessor name (because + // C# allows method overloading on parameter list for the static externs). + if (s.HasGetter && s.GetterIsGeneric && !string.IsNullOrEmpty(s.GetterGenericInteropType)) + { + writer.WriteLine(); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "{{kvp.Key}}")] + static extern {{s.GetterPropTypeText}} {{s.GetterGenericAccessorName}}([UnsafeAccessorType("{{s.GetterGenericInteropType}}")] object _, WindowsRuntimeObjectReference thisReference); + """, isMultiline: true); + } + + if (s.HasSetter && s.SetterIsGeneric && !string.IsNullOrEmpty(s.SetterGenericInteropType)) + { + writer.WriteLine(); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "{{kvp.Key}}")] + static extern void {{s.SetterGenericAccessorName}}([UnsafeAccessorType("{{s.SetterGenericInteropType}}")] object _, WindowsRuntimeObjectReference thisReference, {{s.SetterPropTypeText}} value); + """, isMultiline: true); + } + + writer.WriteLine(); + // when getter and setter platforms match; otherwise emit per-accessor. + string getterPlat = s.GetterPlatformAttribute; + string setterPlat = s.SetterPlatformAttribute; + string propertyPlat = string.Empty; + // If both accessor platform attributes are equal, collapse them to a single + // property-level attribute. For getter-only or setter-only properties only one side + // is set; compare the relevant side. + bool bothSidesPresent = s.HasGetter && s.HasSetter; + + if (!bothSidesPresent || getterPlat == setterPlat) + { + // Collapse: prefer the populated side (treats both-empty as equal). + propertyPlat = !string.IsNullOrEmpty(getterPlat) ? getterPlat : setterPlat; + getterPlat = string.Empty; + setterPlat = string.Empty; + } + + if (!string.IsNullOrEmpty(propertyPlat)) + { + writer.Write(propertyPlat); + } + + writer.Write($"{s.Access}{s.MethodSpec}{s.PropTypeText} {kvp.Key}"); + // For getter-only properties, emit expression body: 'public T Prop => Expr;' + // For getter+setter or setter-only, use accessor block: 'public T Prop { get => ...; set => ...; }' + // In ref mode, all property bodies emit '=> throw null;' + //, 1697). + bool getterOnly = s.HasGetter && !s.HasSetter; + + if (getterOnly) + { + writer.Write(" => "); + + if (context.Settings.ReferenceProjection) + { + writer.Write("throw null;"); + } + else if (s.GetterIsGeneric) + { + if (!string.IsNullOrEmpty(s.GetterGenericInteropType)) + { + writer.Write($"{s.GetterGenericAccessorName}(null, {s.GetterObjRef});"); + } + else + { + writer.Write("throw null!;"); + } + } + else + { + writer.Write($"{s.GetterAbiClass}.{kvp.Key}({s.GetterObjRef});"); + } + + writer.WriteLine(); + } + else + { + writer.WriteLine(); + using (writer.WriteBlock()) + { + if (s.HasGetter) + { + if (!string.IsNullOrEmpty(getterPlat)) + { + writer.Write($"{getterPlat}"); + } + + if (context.Settings.ReferenceProjection) + { + writer.WriteLine("get => throw null;"); + } + else if (s.GetterIsGeneric) + { + if (!string.IsNullOrEmpty(s.GetterGenericInteropType)) + { + writer.WriteLine($"get => {s.GetterGenericAccessorName}(null, {s.GetterObjRef});"); + } + else + { + writer.WriteLine("get => throw null!;"); + } + } + else + { + writer.WriteLine($"get => {s.GetterAbiClass}.{kvp.Key}({s.GetterObjRef});"); + } + } + + if (s.HasSetter) + { + if (!string.IsNullOrEmpty(setterPlat)) + { + writer.Write($"{setterPlat}"); + } + + if (context.Settings.ReferenceProjection) + { + writer.WriteLine("set => throw null;"); + } + else if (s.SetterIsGeneric) + { + if (!string.IsNullOrEmpty(s.SetterGenericInteropType)) + { + writer.WriteLine($"set => {s.SetterGenericAccessorName}(null, {s.SetterObjRef}, value);"); + } + else + { + writer.WriteLine("set => throw null!;"); + } + } + else + { + writer.WriteLine($"set => {s.SetterAbiClass}.{kvp.Key}({s.SetterObjRef}, value);"); + } + } + } + } + + // For overridable properties, emit an explicit interface implementation that + // delegates to the protected property.: + // T InterfaceName.PropName { get => PropName; } + // T InterfaceName.PropName { set => PropName = value; } + if (s.IsOverridable && s.OverridableInterface is not null) + { + writer.Write($"{s.PropTypeText} "); + WriteInterfaceTypeNameForCcw(writer, context, s.OverridableInterface); + writer.Write($".{kvp.Key} {{"); + + if (s.HasGetter) + { + writer.Write($"get => {kvp.Key}; "); + } + + if (s.HasSetter) + { + writer.Write($"set => {kvp.Key} = value; "); + } + + writer.WriteLine("}"); + } + } + + // GetInterface() / GetDefaultInterface() impls are emitted per-interface inside + // WriteInterfaceMembersRecursive (matches the original code's per-interface ordering). + } + + private static string BuildMethodSignatureKey(string name, MethodSignatureInfo sig) + { + System.Text.StringBuilder sb = new(); + _ = sb.Append(name); + _ = sb.Append('('); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + _ = sb.Append(','); + } + + _ = sb.Append(sig.Parameters[i].Type?.FullName ?? "?"); + } + _ = sb.Append(')'); + return sb.ToString(); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs new file mode 100644 index 0000000000..3122670800 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs @@ -0,0 +1,626 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class ClassMembersFactory +{ + private static void WriteInterfaceMembersRecursive(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition classType, TypeDefinition declaringType, + GenericInstanceTypeSignature? currentInstance, + HashSet writtenMethods, IDictionary propertyState, HashSet writtenEvents, HashSet writtenInterfaces) + { + GenericContext genericContext = new(currentInstance, null); + + foreach (InterfaceImplementation impl in declaringType.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + // Resolve TypeRef to TypeDef using our cache + TypeDefinition? ifaceType = ResolveInterface(context.Cache, impl.Interface); + + if (ifaceType is null) + { + continue; + } + + if (writtenInterfaces.Contains(ifaceType)) + { + continue; + } + + _ = writtenInterfaces.Add(ifaceType); + + bool isOverridable = impl.IsOverridable(); + bool isProtected = impl.HasAttribute(WindowsFoundationMetadata, "ProtectedAttribute"); + + // Substitute generic type arguments using the current generic context BEFORE emitting + // any references to this interface. This is critical for nested recursion: e.g. when + // emitting members for IObservableMap's base IMap, we need to + // substitute !0/!1 with string/object so the generated code references + // IDictionary instead of IDictionary. + ITypeDefOrRef substitutedInterface = impl.Interface; + GenericInstanceTypeSignature? nextInstance = null; + + if (impl.Interface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + if (currentInstance is not null) + { + TypeSignature subSig = gi.InstantiateGenericTypes(genericContext); + + if (subSig is GenericInstanceTypeSignature subGi) + { + nextInstance = subGi; + ITypeDefOrRef? newRef = subGi.ToTypeDefOrRef(); + + if (newRef is not null) + { + substitutedInterface = newRef; + } + } + else + { + nextInstance = gi; + } + } + else + { + nextInstance = gi; + } + } + + // Emit GetInterface() / GetDefaultInterface() impl for this interface BEFORE its + // members. For + // overridable interfaces or non-exclusive direct interfaces, emit + // IWindowsRuntimeInterface.GetInterface(). For the default interface on an + // unsealed class with an exclusive default, emit "internal new GetDefaultInterface()". + // The IWindowsRuntimeInterface markers are NOT emitted in ref mode (gated by + // !context.Settings.ReferenceProjection here). The 'internal new + // GetDefaultInterface()' helper IS emitted in both modes since it's referenced by + // overrides on derived classes. + if (IsInterfaceInInheritanceList(context.Cache, impl, includeExclusiveInterface: false) && !context.Settings.ReferenceProjection) + { + string giObjRefName = ObjRefNameGenerator.GetObjRefName(context, substitutedInterface); + writer.WriteLine(); + writer.Write("WindowsRuntimeObjectReferenceValue IWindowsRuntimeInterface<"); + WriteInterfaceTypeNameForCcw(writer, context, substitutedInterface); + writer.WriteLine($$""" + >.GetInterface() + { + return {{giObjRefName}}.AsValue(); + } + """, isMultiline: true); + } + else if (impl.IsDefaultInterface() && !classType.IsSealed) + { + // 'internal new GetDefaultInterface()' helper whenever the interface is the + // default interface and the class is unsealed -- regardless of exclusive-to + // status. In ref-projection mode this is the only branch that emits the helper + // (the prior 'IWindowsRuntimeInterface.GetInterface' branch is gated off). + // In non-ref mode this branch is only reached when the prior branch's + // IsInterfaceInInheritanceList check fails (i.e., ExclusiveTo default interfaces), + // because non-exclusive default interfaces are routed to the prior branch. + string giObjRefName = ObjRefNameGenerator.GetObjRefName(context, substitutedInterface); + bool hasBaseType = false; + + if (classType.BaseType is not null) + { + string? baseNs = classType.BaseType.Namespace?.Value; + string? baseName = classType.BaseType.Name?.Value; + hasBaseType = !(baseNs == "System" && baseName == "Object"); + } + + writer.WriteLine(); + writer.Write("internal "); + + if (hasBaseType) + { + writer.Write("new "); + } + + writer.WriteLine($$""" + WindowsRuntimeObjectReferenceValue GetDefaultInterface() + { + return {{giObjRefName}}.AsValue(); + } + """, isMultiline: true); + } + + // For mapped interfaces with custom members output (e.g. IClosable -> IDisposable, IMap`2 + // -> IDictionary), emit stubs for the C# interface's required members so the class + // satisfies its inheritance contract. The runtime's adapter actually services them. + (string ifaceNs, string ifaceName) = ifaceType.Names(); + + if (MappedTypes.Get(ifaceNs, ifaceName) is { HasCustomMembersOutput: true }) + { + if (MappedInterfaceStubFactory.IsMappedInterfaceRequiringStubs(ifaceNs, ifaceName)) + { + // For generic interfaces, use the substituted nextInstance to compute the + // objref name so type arguments are concrete (matches the field name emitted + // by WriteClassObjRefDefinitions). For non-generic, fall back to impl.Interface. + string objRefName = ObjRefNameGenerator.GetObjRefName(context, substitutedInterface); + MappedInterfaceStubFactory.WriteMappedInterfaceStubs(writer, context, nextInstance, ifaceName, objRefName); + } + + continue; + } + + WriteInterfaceMembers(writer, context, classType, ifaceType, impl.Interface, isOverridable, isProtected, nextInstance, + writtenMethods, propertyState, writtenEvents); + + // Recurse into derived interfaces + WriteInterfaceMembersRecursive(writer, context, classType, ifaceType, nextInstance, writtenMethods, propertyState, writtenEvents, writtenInterfaces); + } + } + + private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition classType, TypeDefinition ifaceType, + ITypeDefOrRef originalInterface, + bool isOverridable, bool isProtected, GenericInstanceTypeSignature? currentInstance, + HashSet writtenMethods, IDictionary propertyState, HashSet writtenEvents) + { + bool sealed_ = classType.IsSealed; + // Determine accessibility and method modifier. + // Overridable interfaces are emitted with 'protected' visibility, plus 'virtual' on + // non-sealed classes. Sealed classes still get 'protected' (without virtual). + string access = (isOverridable || isProtected) ? "protected " : "public "; + string methodSpec = string.Empty; + + if (isOverridable && !sealed_) + { + methodSpec = "virtual "; + } + + GenericContext? genericContext = currentInstance is not null + ? new GenericContext(currentInstance, null) + : null; + + // Generic interfaces require UnsafeAccessor-based dispatch (real ABI lives in the + // post-build interop assembly). + bool isGenericInterface = ifaceType.GenericParameters.Count > 0; + + // Fast ABI: when this interface is exclusive_to a fast-abi class (and we're emitting + // class members, classType is that fast-abi class), dispatch routes through the + // default interface's ABI Methods class and objref instead of through this interface's + // own ABI Methods class. The native vtable bundles all exclusive interfaces' methods + // into the default interface's vtable in a fixed order + TypeDefinition abiInterface = ifaceType; + ITypeDefOrRef abiInterfaceRef = originalInterface; + bool isFastAbiExclusive = ClassFactory.IsFastAbiClass(classType) && TypeCategorization.IsExclusiveTo(ifaceType); + bool isDefaultInterface = false; + + if (isFastAbiExclusive) + { + (TypeDefinition? defaultIface, _) = ClassFactory.GetFastAbiInterfaces(context.Cache, classType); + + if (defaultIface is not null) + { + abiInterface = defaultIface; + abiInterfaceRef = defaultIface; + isDefaultInterface = ReferenceEquals(defaultIface, ifaceType); + } + } + + // '!is_fast_abi_iface || is_default_interface'. For events on a fast-abi non-default + // exclusive interface (e.g. ISimple5.Event0 on the Simple class), the inline + // _eventSource_X field pattern is WRONG: the slot computed from the interface's own + // method index is invalid (the runtime exposes only the merged ISimple vtable, not + // a separate ISimple5 vtable). Instead, dispatch through the default interface's + // ABI Methods class helper (e.g. ISimpleMethods.Event0(this, _objRef_..ISimple)) + // which uses the correct merged-vtable slot and a ConditionalWeakTable for caching. + bool inlineEventSourceField = !isFastAbiExclusive || isDefaultInterface; + + // Compute the ABI Methods static class name (e.g. "global::ABI.Windows.Foundation.IDeferralMethods") + // — note this is the ungenerified Methods class for generic interfaces + // The _objRef_ field name uses the full instantiated interface name so generic instantiations + // (e.g. IAsyncOperation) get a per-instantiation field. + string abiClass = TypedefNameWriter.WriteTypedefName(context, abiInterface, TypedefNameType.StaticAbiClass, true); + + if (!abiClass.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + abiClass = GlobalPrefix + abiClass; + } + + string objRef = ObjRefNameGenerator.GetObjRefName(context, abiInterfaceRef); + + // For generic interfaces, also compute the encoded parent type name (used in UnsafeAccessor + // function names) and the WinRT.Interop accessor type string (passed to UnsafeAccessorType). + string genericParentEncoded = string.Empty; + string genericInteropType = string.Empty; + + if (isGenericInterface && currentInstance is not null) + { + string projectedParent = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(currentInstance), TypedefNameType.Projected, true); + genericParentEncoded = IidExpressionGenerator.EscapeTypeNameForIdentifier(projectedParent, stripGlobal: true); + genericInteropType = InteropTypeNameWriter.EncodeInteropTypeName(currentInstance, TypedefNameType.StaticAbiClass) + ", WinRT.Interop"; + } + + // Compute the platform attribute string from the interface type's [ContractVersion] + // attribute. In ref mode, this is prepended to each member emission so the projected + // class members carry [SupportedOSPlatform("WindowsX.Y.Z.0")] mirroring the interface's + // contract version. Only emitted in ref mode (WritePlatformAttribute internally returns + // immediately if not ref) + string platformAttribute = CustomAttributeFactory.WritePlatformAttribute(context, ifaceType); + + // Methods + foreach (MethodDefinition method in ifaceType.Methods) + { + if (method.IsSpecial()) + { + continue; + } + + string name = method.Name?.Value ?? string.Empty; + // Track by full signature (name + each param's element-type code) to avoid trivial overload duplicates. + // This prevents collapsing distinct overloads like Format(double) and Format(ulong). + MethodSignatureInfo sig = new(method, genericContext); + string key = BuildMethodSignatureKey(name, sig); + + if (!writtenMethods.Add(key)) + { + continue; + } + + // Detect a 'string ToString()' that overrides Object.ToString() and force the + // 'override' modifier on the emitted member. + string methodSpecForThis = methodSpec; + + if (name == "ToString" && sig.Parameters.Count == 0 + && sig.ReturnType is CorLibTypeSignature crt + && crt.ElementType == ElementType.String) + { + methodSpecForThis = "override "; + } + + // Detect 'bool Equals(object obj)' and 'int GetHashCode()' that override their + // System.Object counterparts.h:566 (is_object_equals_method) and + //matching + // signature and return type -> 'override'; matching name only -> 'new'. + if (name == "Equals" && sig.Parameters.Count == 1) + { + TypeSignature p0 = sig.Parameters[0].Type; + bool paramIsObject = p0 is CorLibTypeSignature po + && po.ElementType == ElementType.Object; + bool returnsBool = sig.ReturnType is CorLibTypeSignature ro + && ro.ElementType == ElementType.Boolean; + + if (paramIsObject) + { + methodSpecForThis = returnsBool ? "override " : (methodSpecForThis + "new "); + } + } + else if (name == "GetHashCode" && sig.Parameters.Count == 0) + { + bool returnsInt = sig.ReturnType is CorLibTypeSignature ri + && ri.ElementType == ElementType.I4; + methodSpecForThis = returnsInt ? "override " : (methodSpecForThis + "new "); + } + + if (isGenericInterface && !string.IsNullOrEmpty(genericInteropType)) + { + // Emit UnsafeAccessor static extern + body that dispatches through it. + string accessorName = genericParentEncoded + "_" + name; + writer.WriteLine(); + writer.Write($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "{{name}}")] + static extern + """, isMultiline: true); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {accessorName}([UnsafeAccessorType(\"{genericInteropType}\")] object _, WindowsRuntimeObjectReference thisReference"); + for (int i = 0; i < sig.Parameters.Count; i++) + { + writer.Write(", "); + MethodFactory.WriteProjectionParameter(writer, context, sig.Parameters[i]); + } + writer.WriteLine(");"); + // string to each public method emission. In ref mode this produces e.g. + // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write($"{access}{methodSpecForThis}"); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {name}("); + MethodFactory.WriteParameterList(writer, context, sig); + + if (context.Settings.ReferenceProjection) + { + // which emits 'throw null' in reference projection mode. + writer.WriteLine(") => throw null;"); + } + else + { + writer.Write($") => {accessorName}(null, {objRef}"); + for (int i = 0; i < sig.Parameters.Count; i++) + { + writer.Write(", "); + WriteParameterNameWithModifier(writer, context, sig.Parameters[i]); + } + writer.WriteLine(");"); + } + } + else + { + writer.WriteLine(); + + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write($"{access}{methodSpecForThis}"); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {name}("); + MethodFactory.WriteParameterList(writer, context, sig); + + if (context.Settings.ReferenceProjection) + { + // which emits 'throw null' in reference projection mode. + writer.WriteLine(") => throw null;"); + } + else + { + writer.Write($") => {abiClass}.{name}({objRef}"); + for (int i = 0; i < sig.Parameters.Count; i++) + { + writer.Write(", "); + WriteParameterNameWithModifier(writer, context, sig.Parameters[i]); + } + writer.WriteLine(");"); + } + } + + // For overridable interface methods, emit an explicit interface implementation + // that delegates to the protected (and virtual on non-sealed) method + if (isOverridable) + { + // impl as well (since it shares the same originating interface). + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write(" "); + WriteInterfaceTypeNameForCcw(writer, context, originalInterface); + writer.Write($".{name}("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.Write($") => {name}("); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + WriteParameterNameWithModifier(writer, context, sig.Parameters[i]); + } + writer.WriteLine(");"); + } + } + + // Properties: collect into propertyState (merging accessors from multiple interfaces). + // Track per-accessor origin so that the getter/setter dispatch to the right ABI Methods + // class on the right _objRef_ field. + foreach (PropertyDefinition prop in ifaceType.Properties) + { + string name = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = prop.GetPropertyMethods(); + + if (!propertyState.TryGetValue(name, out PropertyAccessorState? state)) + { + state = new PropertyAccessorState + { + PropTypeText = InterfaceFactory.WritePropType(context, prop, genericContext), + Access = access, + MethodSpec = methodSpec, + IsOverridable = isOverridable, + OverridableInterface = isOverridable ? originalInterface : null, + }; + propertyState[name] = state; + } + + if (getter is not null && !state.HasGetter) + { + state.HasGetter = true; + state.GetterAbiClass = abiClass; + state.GetterObjRef = objRef; + state.GetterIsGeneric = isGenericInterface; + state.GetterGenericInteropType = genericInteropType; + state.GetterGenericAccessorName = isGenericInterface ? (genericParentEncoded + "_" + name) : string.Empty; + state.GetterPropTypeText = InterfaceFactory.WritePropType(context, prop, genericContext); + state.GetterPlatformAttribute = platformAttribute; + } + + if (setter is not null && !state.HasSetter) + { + state.HasSetter = true; + state.SetterAbiClass = abiClass; + state.SetterObjRef = objRef; + state.SetterIsGeneric = isGenericInterface; + state.SetterGenericInteropType = genericInteropType; + state.SetterGenericAccessorName = isGenericInterface ? (genericParentEncoded + "_" + name) : string.Empty; + state.SetterPropTypeText = InterfaceFactory.WritePropType(context, prop, genericContext); + state.SetterPlatformAttribute = platformAttribute; + } + } + + // Events: emit the event with Subscribe/Unsubscribe through a per-event _eventSource_ + // backing property field that lazily constructs an EventHandlerEventSource for the event + // handler type. + foreach (EventDefinition evt in ifaceType.Events) + { + string name = evt.Name?.Value ?? string.Empty; + + if (!writtenEvents.Add(name)) + { + continue; + } + + // Compute event handler type and event source type strings. + TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); + + if (currentInstance is not null) + { + evtSig = evtSig.InstantiateGenericTypes(new GenericContext(currentInstance, null)); + } + + bool isGenericEvent = evtSig is GenericInstanceTypeSignature; + + // Special case for ICommand.CanExecuteChanged: the WinRT event handler is + // EventHandler but C# expects non-generic EventHandler. Use the non-generic + // EventHandlerEventSource backing field. + bool isICommandCanExecuteChanged = name == "CanExecuteChanged" + && (ifaceType.FullName is "Microsoft.UI.Xaml.Input.ICommand" or "Windows.UI.Xaml.Input.ICommand"); + + string eventSourceType; + + if (isICommandCanExecuteChanged) + { + eventSourceType = "global::WindowsRuntime.InteropServices.EventHandlerEventSource"; + isGenericEvent = false; + } + else + { + eventSourceType = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.Get(evtSig), TypedefNameType.EventSource, false); + } + + string eventSourceTypeFull = eventSourceType; + + if (!eventSourceTypeFull.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + eventSourceTypeFull = GlobalPrefix + eventSourceTypeFull; + } + + // The "interop" type name string for the EventSource UnsafeAccessor (only needed for generic events). + string eventSourceInteropType = isGenericEvent + ? InteropTypeNameWriter.EncodeInteropTypeName(evtSig, TypedefNameType.EventSource) + ", WinRT.Interop" + : string.Empty; + + // Compute vtable index = method index in the interface vtable + 6 (for IInspectable methods). + // The add method is the first method of the event in the interface. + int methodIndex = 0; + foreach (MethodDefinition m in ifaceType.Methods) + { + if (m == evt.AddMethod) + { + break; + } + + methodIndex++; + } + int vtableIndex = 6 + methodIndex; + + // Emit the _eventSource_ property field — skipped in ref mode (the event + // accessors below become 'add => throw null;' / 'remove => throw null;' which + // don't reference the field, + if (!context.Settings.ReferenceProjection && inlineEventSourceField) + { + writer.WriteLine(); + writer.WriteLine($$""" + private {{eventSourceTypeFull}} _eventSource_{{name}} + { + get + { + """, isMultiline: true); + if (isGenericEvent && !string.IsNullOrEmpty(eventSourceInteropType)) + { + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.Constructor)] + [return: UnsafeAccessorType("{{eventSourceInteropType}}")] + static extern object ctor(WindowsRuntimeObjectReference nativeObjectReference, int index); + """, isMultiline: true); + writer.WriteLine(); + } + writer.Write($$""" + [MethodImpl(MethodImplOptions.NoInlining)] + {{eventSourceTypeFull}} MakeEventSource() + { + _ = global::System.Threading.Interlocked.CompareExchange( + location1: ref field, + value: + """, isMultiline: true); + if (isGenericEvent) + { + writer.Write($"Unsafe.As<{eventSourceTypeFull}>(ctor({objRef}, {vtableIndex.ToString(CultureInfo.InvariantCulture)}))"); + } + else + { + writer.Write($"new {eventSourceTypeFull}({objRef}, {vtableIndex.ToString(CultureInfo.InvariantCulture)})"); + } + + writer.WriteLine(""" + , + comparand: null); + + return field; + } + + return field ?? MakeEventSource(); + } + } + """, isMultiline: true); + } + + // Emit the public/protected event with Subscribe/Unsubscribe. + writer.WriteLine(); + // string to each event emission. In ref mode this produces e.g. + // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write($"{access}{methodSpec}event "); + TypedefNameWriter.WriteEventType(writer, context, evt, currentInstance); + writer.WriteLine($$""" + {{name}} + { + """, isMultiline: true); + if (context.Settings.ReferenceProjection) + { + writer.WriteLine(""" + add => throw null; + remove => throw null; + """, isMultiline: true); + } + else if (inlineEventSourceField) + { + writer.WriteLine($$""" + add => _eventSource_{{name}}.Subscribe(value); + remove => _eventSource_{{name}}.Unsubscribe(value); + """, isMultiline: true); + } + else + { + // Fast-abi non-default exclusive: dispatch through the default interface's + // ABI Methods class helper.h write_event when + // inline_event_source_field is false (the default helper-based path). + // Example: Simple.Event0 (on ISimple5) becomes + // add => global::ABI.test_component_fast.ISimpleMethods.Event0((WindowsRuntimeObject)this, _objRef_test_component_fast_ISimple).Subscribe(value); + writer.WriteLine($$""" + add => {{abiClass}}.{{name}}((WindowsRuntimeObject)this, {{objRef}}).Subscribe(value); + remove => {{abiClass}}.{{name}}((WindowsRuntimeObject)this, {{objRef}}).Unsubscribe(value); + """, isMultiline: true); + } + writer.WriteLine("}"); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.cs new file mode 100644 index 0000000000..31b8420545 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the per-class member surface (instance methods, properties, events) plus the +/// per-required-interface DIM thunks for runtime classes. +/// +/// +/// The implementation is split across several partial files: +/// +/// ClassMembersFactory.WriteClassMembers.cs - top-level class-member emission entry point + the per-method dedupe key helper. +/// ClassMembersFactory.WriteInterfaceMembers.cs - per-required-interface member emission (recursive walk + the per-interface emitter). +/// +/// +internal static partial class ClassMembersFactory +{ + /// + /// Returns true if the given interface implementation should appear in the class's inheritance list + /// (i.e., it has [Overridable], or is not [ExclusiveTo], or includeExclusiveInterface is set). + /// + internal static bool IsInterfaceInInheritanceList(MetadataCache cache, InterfaceImplementation impl, bool includeExclusiveInterface) + { + if (impl.Interface is null) + { + return false; + } + + if (impl.IsOverridable()) + { + return true; + } + + if (includeExclusiveInterface) + { + return true; + } + + TypeDefinition? td = ResolveInterface(cache, impl.Interface); + + if (td is null) + { + return true; + } + + return !TypeCategorization.IsExclusiveTo(td); + } + internal static TypeDefinition? ResolveInterface(MetadataCache cache, ITypeDefOrRef typeRef) + { + if (typeRef is TypeDefinition td) + { + return td; + } + + TypeDefinition? resolved = typeRef.TryResolve(cache.RuntimeContext); + + if (resolved is not null) + { + return resolved; + } + + // Fall back to local lookup by full name + if (typeRef is TypeReference tr) + { + (string ns, string name) = tr.Names(); + string fullName = string.IsNullOrEmpty(ns) ? name : ns + "." + name; + return cache.Find(fullName); + } + + if (typeRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + return ResolveInterface(cache, gi.GenericType); + } + + return null; + } + + /// + /// Writes a parameter name prefixed with its modifier (in/out/ref) for use as a call argument. + /// + internal static void WriteParameterNameWithModifier(IndentedTextWriter writer, ProjectionEmitContext context, ParameterInfo p) + { + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + switch (cat) + { + case ParameterCategory.Out: + writer.Write("out "); + break; + case ParameterCategory.Ref: + writer.Write("in "); + break; + case ParameterCategory.ReceiveArray: + writer.Write("out "); + break; + } + MethodFactory.WriteParameterName(writer, p); + } + + /// + /// Writes the projected name for an interface reference (TypeDefinition, TypeReference, or + /// generic instance), applying mapped-type remapping. Used inside IWindowsRuntimeInterface<T>. + /// + internal static void WriteInterfaceTypeNameForCcw(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + // If the reference is to a type in the same module, resolve to TypeDefinition so + // WriteTypedefName can drop the 'global::.' prefix when the namespace matches. + if (ifaceType is not TypeDefinition && ifaceType is not TypeSpecification && context.Cache is not null) + { + TypeDefinition? resolved = ifaceType.TryResolve(context.Cache.RuntimeContext); + + if (resolved is not null) + { + ifaceType = resolved; + } + } + + if (ifaceType is TypeDefinition td) + { + TypedefNameWriter.WriteTypedefName(writer, context, td, TypedefNameType.CCW, false); + TypedefNameWriter.WriteTypeParams(writer, td); + } + else if (ifaceType is TypeReference tr) + { + (string ns, string name) = tr.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + writer.Write($"global::{ns}.{IdentifierEscaping.StripBackticks(name)}"); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef gt = gi.GenericType; + (string ns, string name) = gt.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + writer.Write($"global::{ns}.{IdentifierEscaping.StripBackticks(name)}<"); + for (int i = 0; i < gi.TypeArguments.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + TypedefNameWriter.WriteTypeName(writer, context, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, true); + } + writer.Write(">"); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs new file mode 100644 index 0000000000..1cbe248be8 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Component-mode helpers. +/// +internal static class ComponentFactory +{ + /// + /// Adds a (projected -> CCW) type-name pair to the metadata-type map. + /// + public static void AddMetadataTypeEntry(ProjectionEmitContext context, TypeDefinition type, ConcurrentDictionary map) + { + if (!context.Settings.Component) + { + return; + } + + TypeCategory cat = TypeCategorization.GetCategory(type); + + if ((cat == TypeCategory.Class && TypeCategorization.IsStatic(type)) || + (cat == TypeCategory.Interface && TypeCategorization.IsExclusiveTo(type))) + { + return; + } + + string typeName = TypedefNameWriter.WriteTypedefNameWithTypeParams(context, type, TypedefNameType.Projected, true); + + string metadataTypeName = TypedefNameWriter.WriteTypedefNameWithTypeParams(context, type, TypedefNameType.CCW, true); + + _ = map.TryAdd(typeName, metadataTypeName); + } + + /// + /// Writes the per-runtime-class server-activation-factory type for component mode. + /// + public static void WriteFactoryClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string typeName = type.Name?.Value ?? string.Empty; + string typeNs = type.Namespace?.Value ?? string.Empty; + string projectedTypeName = string.IsNullOrEmpty(typeNs) + ? $"global::{IdentifierEscaping.StripBackticks(typeName)}" + : $"global::{typeNs}.{IdentifierEscaping.StripBackticks(typeName)}"; + string factoryTypeName = $"{IdentifierEscaping.StripBackticks(typeName)}ServerActivationFactory"; + bool isActivatable = !TypeCategorization.IsStatic(type) && type.HasDefaultConstructor(); + + // Build the inheritance list: factory interfaces ([Activatable]/[Static]) only. + MetadataCache cache = context.Cache; + List factoryInterfaces = []; + foreach (KeyValuePair kv in AttributedTypes.Get(type, cache)) + { + AttributedType info = kv.Value; + + if ((info.Activatable || info.Statics) && info.Type is not null) + { + factoryInterfaces.Add(info.Type); + } + } + + writer.WriteLine(); + writer.Write($"internal sealed class {factoryTypeName} : global::WindowsRuntime.InteropServices.IActivationFactory"); + foreach (TypeDefinition iface in factoryInterfaces) + { + writer.Write(", "); + // CCW + non-forced namespace is the user-facing interface name (e.g. 'IButtonUtilsStatic'). + TypedefNameWriter.WriteTypedefName(writer, context, iface, TypedefNameType.CCW, false); + TypedefNameWriter.WriteTypeParams(writer, iface); + } + writer.WriteLine(); + writer.WriteLine($$""" + { + static {{factoryTypeName}}() + { + global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof({{projectedTypeName}}).TypeHandle); + } + + public static unsafe void* Make() + { + return global::WindowsRuntime.InteropServices.Marshalling.WindowsRuntimeInterfaceMarshaller + .ConvertToUnmanaged(_factory, in global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IActivationFactory) + .DetachThisPtrUnsafe(); + } + + private static readonly {{factoryTypeName}} _factory = new(); + + public object ActivateInstance() + { + """, isMultiline: true); + if (isActivatable) + { + writer.Write($"return new {projectedTypeName}();"); + } + else + { + writer.Write("throw new NotImplementedException();"); + } + + writer.WriteLine(); + writer.WriteLine("}"); + + // Emit factory-class members: forwarding methods/properties/events for static factory + // interfaces, and constructor wrappers for activatable factory interfaces. + if (cache is not null) + { + foreach (KeyValuePair kv in AttributedTypes.Get(type, cache)) + { + AttributedType info = kv.Value; + + if (info.Type is null) + { + continue; + } + + if (info.Activatable) + { + foreach (MethodDefinition method in info.Type.Methods) + { + if (method.IsConstructor) + { + continue; + } + + WriteFactoryActivatableMethod(writer, context, method, projectedTypeName); + } + } + else if (info.Statics) + { + foreach (MethodDefinition method in info.Type.Methods) + { + if (method.IsConstructor) + { + continue; + } + + WriteStaticFactoryMethod(writer, context, method, projectedTypeName); + } + foreach (PropertyDefinition prop in info.Type.Properties) + { + WriteStaticFactoryProperty(writer, context, prop, projectedTypeName); + } + foreach (EventDefinition evt in info.Type.Events) + { + WriteStaticFactoryEvent(writer, context, evt, projectedTypeName); + } + } + } + } + + writer.WriteLine("}"); + } + + /// + /// Writes a factory-class activatable wrapper method: + /// public T MethodName(args) => new T(args);. + /// + private static void WriteFactoryActivatableMethod(IndentedTextWriter writer, ProjectionEmitContext context, MethodDefinition method, string projectedTypeName) + { + if (method.IsSpecialName) + { + return; + } + + string methodName = method.Name?.Value ?? string.Empty; + writer.WriteLine(); + writer.Write($"public {projectedTypeName} {methodName}("); + WriteFactoryMethodParameters(writer, context, method, includeTypes: true); + writer.Write($") => new {projectedTypeName}("); + WriteFactoryMethodParameters(writer, context, method, includeTypes: false); + writer.WriteLine(");"); + } + + /// + /// Writes a static-factory forwarding method: + /// public Ret MethodName(args) => global::Ns.Type.MethodName(args);. + /// + private static void WriteStaticFactoryMethod(IndentedTextWriter writer, ProjectionEmitContext context, MethodDefinition method, string projectedTypeName) + { + if (method.IsSpecialName) + { + return; + } + + string methodName = method.Name?.Value ?? string.Empty; + writer.WriteLine(); + writer.Write("public "); + WriteFactoryReturnType(writer, context, method); + writer.Write($" {methodName}("); + WriteFactoryMethodParameters(writer, context, method, includeTypes: true); + writer.Write($") => {projectedTypeName}.{methodName}("); + WriteFactoryMethodParameters(writer, context, method, includeTypes: false); + writer.WriteLine(");"); + } + + /// + /// Writes a static-factory forwarding property (single-line getter or full block). + /// + private static void WriteStaticFactoryProperty(IndentedTextWriter writer, ProjectionEmitContext context, PropertyDefinition prop, string projectedTypeName) + { + string propName = prop.Name?.Value ?? string.Empty; + (MethodDefinition? getter, MethodDefinition? setter) = prop.GetPropertyMethods(); + // Single-line form when no setter is present. + if (setter is null) + { + writer.WriteLine(); + writer.Write("public "); + WriteFactoryPropertyType(writer, context, prop); + writer.WriteLine($" {propName} => {projectedTypeName}.{propName};"); + return; + } + + writer.WriteLine(); + writer.Write("public "); + WriteFactoryPropertyType(writer, context, prop); + writer.WriteLine($$""" + {{propName}} + { + """, isMultiline: true); + if (getter is not null) + { + writer.WriteLine($"get => {projectedTypeName}.{propName};"); + } + + writer.WriteLine($$""" + set => {{projectedTypeName}}.{{propName}} = value; + } + """, isMultiline: true); + } + + /// + /// Writes a static-factory forwarding event as a multi-line block. + /// + private static void WriteStaticFactoryEvent(IndentedTextWriter writer, ProjectionEmitContext context, EventDefinition evt, string projectedTypeName) + { + string evtName = evt.Name?.Value ?? string.Empty; + writer.WriteLine(); + writer.Write("public event "); + + if (evt.EventType is not null) + { + TypeSemantics evtSemantics = TypeSemanticsFactory.GetFromTypeDefOrRef(evt.EventType); + TypedefNameWriter.WriteTypeName(writer, context, evtSemantics, TypedefNameType.Projected, false); + } + + writer.WriteLine($$""" + {{evtName}} + { + add => {{projectedTypeName}}.{{evtName}} += value; + remove => {{projectedTypeName}}.{{evtName}} -= value; + } + """, isMultiline: true); + } + + private static void WriteFactoryReturnType(IndentedTextWriter writer, ProjectionEmitContext context, MethodDefinition method) + { + TypeSignature? returnType = method.Signature?.ReturnType; + + if (returnType is null || returnType.ElementType == ElementType.Void) + { + writer.Write("void"); + return; + } + + TypeSemantics semantics = TypeSemanticsFactory.Get(returnType); + TypedefNameWriter.WriteTypeName(writer, context, semantics, TypedefNameType.Projected, true); + } + + private static void WriteFactoryPropertyType(IndentedTextWriter writer, ProjectionEmitContext context, PropertyDefinition prop) + { + TypeSignature? sig = prop.Signature?.ReturnType; + + if (sig is null) + { + writer.Write("object"); return; + } + + TypeSemantics semantics = TypeSemanticsFactory.Get(sig); + TypedefNameWriter.WriteTypeName(writer, context, semantics, TypedefNameType.Projected, true); + } + + private static void WriteFactoryMethodParameters(IndentedTextWriter writer, ProjectionEmitContext context, MethodDefinition method, bool includeTypes) + { + MethodSignature? sig = method.Signature; + + if (sig is null) + { + return; + } + + for (int i = 0; i < sig.ParameterTypes.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + ParameterDefinition? p = method.Parameters.Count > i ? method.Parameters[i].Definition : null; + string paramName = p?.Name?.Value ?? $"arg{i}"; + + if (includeTypes) + { + TypeSemantics semantics = TypeSemanticsFactory.Get(sig.ParameterTypes[i]); + TypedefNameWriter.WriteTypeName(writer, context, semantics, TypedefNameType.Projected, true); + writer.Write($" {paramName}"); + } + else + { + writer.Write(paramName); + } + } + } + + /// + /// Writes the per-module activation-factory dispatch helper. + /// + public static void WriteModuleActivationFactory(IndentedTextWriter writer, IReadOnlyDictionary> typesByModule) + { + writer.WriteLine(); + writer.WriteLine("using System;"); + foreach (KeyValuePair> kv in typesByModule) + { + writer.WriteLine(); + writer.WriteLine($$""" + namespace ABI.{{kv.Key}} + { + public static class ManagedExports + { + public static unsafe void* GetActivationFactory(ReadOnlySpan activatableClassId) + { + switch (activatableClassId) + { + """, isMultiline: true); + // Sort by the type's metadata token / row index so cases appear in WinMD declaration order. + List orderedTypes = [.. kv.Value]; + orderedTypes.Sort((a, b) => + { + uint ra = a.MetadataToken.Rid; + uint rb = b.MetadataToken.Rid; + return ra.CompareTo(rb); + }); + foreach (TypeDefinition type in orderedTypes) + { + (string ns, string name) = type.Names(); + writer.WriteLine($$""" + case "{{ns}}.{{name}}": + return global::ABI.Impl.{{ns}}.{{IdentifierEscaping.StripBackticks(name)}}ServerActivationFactory.Make(); + """, isMultiline: true); + } + writer.WriteLine(""" + default: + return null; + } + } + } + } + """, isMultiline: true); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs new file mode 100644 index 0000000000..1d89c7a833 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Globalization; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class ConstructorFactory +{ + /// + /// Emits the activator and composer constructor wrappers for the given runtime class. + /// + public static void WriteAttributedTypes(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition classType) + { + if (context.Cache is null) + { + return; + } + + // Track whether we need to emit the static _objRef_ field (used by + // default constructors). Emit it once per class if any [Activatable] factory exists. + bool needsClassObjRef = false; + + foreach (KeyValuePair kv in AttributedTypes.Get(classType, context.Cache)) + { + AttributedType factory = kv.Value; + + if (factory.Activatable && factory.Type is null) + { + needsClassObjRef = true; + break; + } + } + + if (needsClassObjRef) + { + string fullName = (classType.Namespace?.Value ?? string.Empty) + "." + (classType.Name?.Value ?? string.Empty); + string objRefName = "_objRef_" + IidExpressionGenerator.EscapeTypeNameForIdentifier(GlobalPrefix + fullName, stripGlobal: true); + writer.WriteLine(); + writer.Write($"private static WindowsRuntimeObjectReference {objRefName}"); + + if (context.Settings.ReferenceProjection) + { + // in ref mode the activation factory objref getter body is just 'throw null;'. + RefModeStubFactory.EmitRefModeObjRefGetterBody(writer); + } + else + { + writer.WriteLine(); + writer.WriteLine($$""" + { + get + { + var __{{objRefName}} = field; + if (__{{objRefName}} != null && __{{objRefName}}.IsInCurrentContext) + { + return __{{objRefName}}; + } + return field = WindowsRuntimeObjectReference.GetActivationFactory("{{fullName}}"); + } + } + """, isMultiline: true); + } + } + + foreach (KeyValuePair kv in AttributedTypes.Get(classType, context.Cache)) + { + AttributedType factory = kv.Value; + + if (factory.Activatable) + { + WriteFactoryConstructors(writer, context, factory.Type, classType); + } + else if (factory.Composable) + { + WriteComposableConstructors(writer, context, factory.Type, classType, factory.Visible ? "public" : "protected"); + } + } + } + + /// + /// Emits the public constructors generated from a [Activatable] factory type. + /// + public static void WriteFactoryConstructors(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition? factoryType, TypeDefinition classType) + { + string typeName = classType.Name?.Value ?? string.Empty; + int gcPressure = ClassFactory.GetGcPressureAmount(classType); + + if (factoryType is not null) + { + // Emit the factory objref property (lazy-initialized). + string factoryRuntimeClassFullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; + string factoryObjRefName = ObjRefNameGenerator.GetObjRefName(context, factoryType); + ClassFactory.WriteStaticFactoryObjRef(writer, context, factoryType, factoryRuntimeClassFullName, factoryObjRefName); + + string defaultIfaceIid = GetDefaultInterfaceIid(context, classType); + string marshalingType = GetMarshalingTypeName(classType); + // Compute the platform attribute string from the activation factory interface's + // [ContractVersion] attribute + string platformAttribute = CustomAttributeFactory.WritePlatformAttribute(context, factoryType); + int methodIndex = 0; + foreach (MethodDefinition method in factoryType.Methods) + { + if (method.IsSpecial()) + { + methodIndex++; continue; + } + + MethodSignatureInfo sig = new(method); + string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Parameters.Count.ToString(CultureInfo.InvariantCulture); + string argsName = callbackName + "Args"; + + // Emit the public constructor. + writer.WriteLine(); + + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write($"public unsafe {typeName}("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.Write(""" + ) + :base( + """, isMultiline: true); + if (sig.Parameters.Count == 0) + { + writer.Write("default"); + } + else + { + writer.Write($"{callbackName}.Instance, {defaultIfaceIid}, {marshalingType}, WindowsRuntimeActivationArgsReference.CreateUnsafe(new {argsName}("); + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + string raw = sig.Parameters[i].Parameter.Name ?? "param"; + writer.Write(CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw); + } + writer.Write("))"); + } + + writer.WriteLine(""" + ) + { + """, isMultiline: true); + if (gcPressure > 0) + { + writer.WriteLine($"GC.AddMemoryPressure({gcPressure.ToString(CultureInfo.InvariantCulture)});"); + } + + writer.WriteLine("}"); + + if (sig.Parameters.Count > 0) + { + EmitFactoryArgsStruct(writer, context, sig, argsName); + EmitFactoryCallbackClass(writer, context, sig, callbackName, argsName, factoryObjRefName, methodIndex); + } + + methodIndex++; + } + } + else + { + // No factory type means [Activatable(uint version)] - emit a default ctor that calls + // the WindowsRuntimeObject base constructor with the activation factory objref. + // The default interface IID is needed too. + string fullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; + string objRefName = "_objRef_" + IidExpressionGenerator.EscapeTypeNameForIdentifier(GlobalPrefix + fullName, stripGlobal: true); + + // Find the default interface IID to use. + string defaultIfaceIid = GetDefaultInterfaceIid(context, classType); + + writer.WriteLine(); + writer.WriteLine($$""" + public {{typeName}}() + :base(default(WindowsRuntimeActivationTypes.DerivedSealed), {{objRefName}}, {{defaultIfaceIid}}, {{GetMarshalingTypeName(classType)}}) + { + """, isMultiline: true); + if (gcPressure > 0) + { + writer.WriteLine($"GC.AddMemoryPressure({gcPressure.ToString(CultureInfo.InvariantCulture)});"); + } + + writer.WriteLine("}"); + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs new file mode 100644 index 0000000000..58093796b2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Globalization; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class ConstructorFactory +{ + /// + /// Emits: + /// 1. Public/protected constructors for each composable factory method (with proper body). + /// 2. Static factory callback class (per ctor) for parameterized composable activation. + /// 3. Four protected base-chaining constructors used by derived projected types. + /// + public static void WriteComposableConstructors(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition? composableType, TypeDefinition classType, string visibility) + { + if (composableType is null) + { + return; + } + + string typeName = classType.Name?.Value ?? string.Empty; + + // Emit the factory objref + IIDs at the top so the parameterized ctors can reference it. + if (composableType.Methods.Count > 0) + { + string runtimeClassFullName = (classType.Namespace?.Value ?? string.Empty) + "." + typeName; + string factoryObjRefName = ObjRefNameGenerator.GetObjRefName(context, composableType); + ClassFactory.WriteStaticFactoryObjRef(writer, context, composableType, runtimeClassFullName, factoryObjRefName); + } + + string defaultIfaceIid = GetDefaultInterfaceIid(context, classType); + string marshalingType = GetMarshalingTypeName(classType); + string defaultIfaceObjRef; + ITypeDefOrRef? defaultIface = classType.GetDefaultInterface(); + defaultIfaceObjRef = defaultIface is not null ? ObjRefNameGenerator.GetObjRefName(context, defaultIface) : string.Empty; + int gcPressure = ClassFactory.GetGcPressureAmount(classType); + // Compute the platform attribute string from the composable factory interface's + // [ContractVersion] attribute + string platformAttribute = CustomAttributeFactory.WritePlatformAttribute(context, composableType); + + int methodIndex = 0; + foreach (MethodDefinition method in composableType.Methods) + { + if (method.IsSpecial()) + { + methodIndex++; continue; + } + + // Composable factory methods have signature like: + // T CreateInstance(args, object baseInterface, out object innerInterface) + // For the constructor on the projected class, we exclude the trailing two params. + MethodSignatureInfo sig = new(method); + int userParamCount = sig.Parameters.Count >= 2 ? sig.Parameters.Count - 2 : sig.Parameters.Count; + // the callback / args type name suffix is the TOTAL ABI param count + // (size(method.Signature().Parameters())), NOT the user-visible param count. Using the + // total count guarantees uniqueness against other composable factory overloads that + // might share the same user-param count but differ in trailing baseInterface shape. + string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Parameters.Count.ToString(CultureInfo.InvariantCulture); + string argsName = callbackName + "Args"; + bool isParameterless = userParamCount == 0; + + writer.WriteLine(); + + if (!string.IsNullOrEmpty(platformAttribute)) + { + writer.Write(platformAttribute); + } + + writer.Write(visibility); + + if (!isParameterless) + { + writer.Write(" unsafe "); + } + else + { + writer.Write(" "); + } + + writer.Write($"{typeName}("); + for (int i = 0; i < userParamCount; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + MethodFactory.WriteProjectionParameter(writer, context, sig.Parameters[i]); + } + + writer.Write(""" + ) + :base( + """, isMultiline: true); + if (isParameterless) + { + // base(default(WindowsRuntimeActivationTypes.DerivedComposed), , , ) + string factoryObjRef = ObjRefNameGenerator.GetObjRefName(context, composableType); + writer.Write($"default(WindowsRuntimeActivationTypes.DerivedComposed), {factoryObjRef}, {defaultIfaceIid}, {marshalingType}"); + } + else + { + writer.Write($"{callbackName}.Instance, {defaultIfaceIid}, {marshalingType}, WindowsRuntimeActivationArgsReference.CreateUnsafe(new {argsName}("); + for (int i = 0; i < userParamCount; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + string raw = sig.Parameters[i].Parameter.Name ?? "param"; + writer.Write(CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw); + } + writer.Write("))"); + } + + writer.WriteLine(""" + ) + """, isMultiline: true); + using (writer.WriteBlock()) + { + writer.WriteLine($$""" + if (GetType() == typeof({{typeName}})) + { + {{defaultIfaceObjRef}} = NativeObjectReference; + } + """, isMultiline: true); + + if (gcPressure > 0) + { + writer.WriteLine($"GC.AddMemoryPressure({gcPressure.ToString(CultureInfo.InvariantCulture)});"); + } + } + + // Emit args struct + callback class for parameterized composable factories. + // skips both the args struct AND the callback class entirely in ref mode. The + // public ctor above still references these types, but reference assemblies don't + // need their bodies' references to resolve (only the public API surface matters). + if (!isParameterless && !context.Settings.ReferenceProjection) + { + EmitFactoryArgsStruct(writer, context, sig, argsName, userParamCount); + string factoryObjRefName = ObjRefNameGenerator.GetObjRefName(context, composableType); + EmitFactoryCallbackClass(writer, context, sig, callbackName, argsName, factoryObjRefName, methodIndex, isComposable: true, userParamCount: userParamCount); + } + + methodIndex++; + } + + if (context.Settings.ReferenceProjection) + { + return; + } + + // Emit the four base-chaining constructors used by derived projected types. + string gcPressureBody = gcPressure > 0 + ? "GC.AddMemoryPressure(" + gcPressure.ToString(CultureInfo.InvariantCulture) + ");" + : string.Empty; + + // 1. WindowsRuntimeActivationTypes.DerivedComposed + writer.WriteLine(); + writer.WriteLine($$""" + protected {{typeName}}(WindowsRuntimeActivationTypes.DerivedComposed _, WindowsRuntimeObjectReference activationFactoryObjectReference, in Guid iid, CreateObjectReferenceMarshalingType marshalingType) + :base(_, activationFactoryObjectReference, in iid, marshalingType) + """, isMultiline: true); + using (writer.WriteBlock()) + { + if (!string.IsNullOrEmpty(gcPressureBody)) + { + writer.WriteLine(gcPressureBody); + } + } + + writer.WriteLine(); + writer.WriteLine($$""" + protected {{typeName}}(WindowsRuntimeActivationTypes.DerivedSealed _, WindowsRuntimeObjectReference activationFactoryObjectReference, in Guid iid, CreateObjectReferenceMarshalingType marshalingType) + :base(_, activationFactoryObjectReference, in iid, marshalingType) + """, isMultiline: true); + using (writer.WriteBlock()) + { + if (!string.IsNullOrEmpty(gcPressureBody)) + { + writer.WriteLine(gcPressureBody); + } + } + + writer.WriteLine(); + writer.WriteLine($$""" + protected {{typeName}}(WindowsRuntimeActivationFactoryCallback.DerivedComposed activationFactoryCallback, in Guid iid, CreateObjectReferenceMarshalingType marshalingType, WindowsRuntimeActivationArgsReference additionalParameters) + :base(activationFactoryCallback, in iid, marshalingType, additionalParameters) + """, isMultiline: true); + using (writer.WriteBlock()) + { + if (!string.IsNullOrEmpty(gcPressureBody)) + { + writer.WriteLine(gcPressureBody); + } + } + + writer.WriteLine(); + writer.WriteLine($$""" + protected {{typeName}}(WindowsRuntimeActivationFactoryCallback.DerivedSealed activationFactoryCallback, in Guid iid, CreateObjectReferenceMarshalingType marshalingType, WindowsRuntimeActivationArgsReference additionalParameters) + :base(activationFactoryCallback, in iid, marshalingType, additionalParameters) + """, isMultiline: true); + using (writer.WriteBlock()) + { + if (!string.IsNullOrEmpty(gcPressureBody)) + { + writer.WriteLine(gcPressureBody); + } + } + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.FactoryCallbacks.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.FactoryCallbacks.cs new file mode 100644 index 0000000000..f1766cefc8 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.FactoryCallbacks.cs @@ -0,0 +1,688 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Globalization; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +internal static partial class ConstructorFactory +{ + /// + /// Emits the private readonly ref struct <Name>Args(args...) {...}. + /// + /// The writer to emit to. + /// The active emit context. + /// The factory method signature whose parameters are turned into struct fields. + /// The simple name of the emitted args struct. + /// If >= 0, only emit the first + /// params (used for composable factories where the trailing baseInterface/innerInterface params + /// are consumed by the callback Invoke signature directly, not stored in args). + private static void EmitFactoryArgsStruct(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig, string argsName, int userParamCount = -1) + { + int count = userParamCount >= 0 ? userParamCount : sig.Parameters.Count; + writer.WriteLine(); + writer.Write($"private readonly ref struct {argsName}("); + for (int i = 0; i < count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + MethodFactory.WriteProjectionParameter(writer, context, sig.Parameters[i]); + } + writer.WriteLine(""" + ) + { + """, isMultiline: true); + for (int i = 0; i < count; i++) + { + ParameterInfo p = sig.Parameters[i]; + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.Write(" public readonly "); + // Use the parameter's projected type (matches the constructor parameter type, including + // ReadOnlySpan/Span for array params). + MethodFactory.WriteProjectionParameterType(writer, context, p); + writer.WriteLine($" {pname} = {pname};"); + } + writer.WriteLine("}"); + } + + /// + /// Emits the private sealed class <Name> : WindowsRuntimeActivationFactoryCallback.DerivedSealed. + /// + /// The writer to emit to. + /// The active emit context. + /// The factory method signature. + /// The simple name of the emitted callback class. + /// The simple name of the args struct previously emitted by . + /// The name of the static lazy WindowsRuntimeObjectReference property holding the activation factory. + /// The vtable slot of the factory method on the activation factory interface. + /// When true, emit the DerivedComposed callback variant whose + /// Invoke signature includes the additional WindowsRuntimeObject baseInterface + + /// out void* innerInterface params. Iteration over user params is bounded by + /// (defaults to all params). + /// If >= 0, only emit the first user params (used for composable factories). + private static void EmitFactoryCallbackClass(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig, string callbackName, string argsName, string factoryObjRefName, int factoryMethodIndex, bool isComposable = false, int userParamCount = -1) + { + int paramCount = userParamCount >= 0 ? userParamCount : sig.Parameters.Count; + string baseClass = isComposable + ? "WindowsRuntimeActivationFactoryCallback.DerivedComposed" + : "WindowsRuntimeActivationFactoryCallback.DerivedSealed"; + writer.WriteLine(); + writer.WriteLine($$""" + private sealed class {{callbackName}} : {{baseClass}} + { + public static readonly {{callbackName}} Instance = new(); + + [MethodImpl(MethodImplOptions.NoInlining)] + """, isMultiline: true); + if (isComposable) + { + // Composable Invoke signature is multi-line and includes baseInterface (in) + + // innerInterface (out). + writer.WriteLine(""" + public override unsafe void Invoke( + WindowsRuntimeActivationArgsReference additionalParameters, + WindowsRuntimeObject baseInterface, + out void* innerInterface, + out void* retval) + { + """, isMultiline: true); + } + else + { + // Sealed Invoke signature is multi-line.. + writer.WriteLine(""" + public override unsafe void Invoke( + WindowsRuntimeActivationArgsReference additionalParameters, + out void* retval) + { + """, isMultiline: true); + } + + // Invoke body is just 'throw null;' (no factory dispatch, no marshalling). + if (context.Settings.ReferenceProjection) + { + RefModeStubFactory.EmitRefModeInvokeBody(writer); + return; + } + + writer.WriteLine($$""" + using WindowsRuntimeObjectReferenceValue activationFactoryValue = {{factoryObjRefName}}.AsValue(); + void* ThisPtr = activationFactoryValue.GetThisPtrUnsafe(); + ref readonly {{argsName}} args = ref additionalParameters.GetValueRefUnsafe<{{argsName}}>(); + """, isMultiline: true); + + // Bind each arg from the args struct to a local of its ABI-marshalable input type. + // Bind arg locals. + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + writer.Write(" "); + // For array params, the bind type is ReadOnlySpan / Span (not the SzArray). + if (cat == ParameterCategory.PassArray) + { + writer.Write("ReadOnlySpan<"); + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); + writer.Write(">"); + } + else if (cat == ParameterCategory.FillArray) + { + writer.Write("Span<"); + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); + writer.Write(">"); + } + else + { + MethodFactory.WriteProjectedSignature(writer, context, p.Type, true); + } + + writer.WriteLine($" {pname} = args.{pname};"); + } + + // For generic instance params, emit local UnsafeAccessor delegates (or Nullable -> BoxToUnmanaged). + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (!p.Type.IsGenericInstance()) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + + if (p.Type.IsNullableT()) + { + TypeSignature inner = p.Type.GetNullableInnerType()!; + string innerMarshaller = AbiTypeHelpers.GetNullableInnerMarshallerName(writer, context, inner); + writer.WriteLine($" using WindowsRuntimeObjectReferenceValue __{raw} = {innerMarshaller}.BoxToUnmanaged({pname});"); + continue; + } + + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(p.Type, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, p.Type, false); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToUnmanaged")] + static extern WindowsRuntimeObjectReferenceValue ConvertToUnmanaged_{{raw}}([UnsafeAccessorType("{{interopTypeName}}")] object _, {{projectedTypeName}} value); + using WindowsRuntimeObjectReferenceValue __{{raw}} = ConvertToUnmanaged_{{raw}}(null, {{pname}}); + """, isMultiline: true); + } + + // For runtime class / object params, emit `using WindowsRuntimeObjectReferenceValue __ = ...ConvertToUnmanaged();` + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + + // already handled above + if (p.Type.IsGenericInstance()) + { + continue; + } + + if (!context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(p.Type) && !p.Type.IsObject()) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.Write($" using WindowsRuntimeObjectReferenceValue __{raw} = "); + AbiMethodBodyFactory.EmitMarshallerConvertToUnmanaged(writer, context, p.Type, pname); + writer.WriteLine(";"); + } + + // For composable factories, marshal the additional `baseInterface` (which is a + // WindowsRuntimeObject parameter on Invoke, not an args field). + // using WindowsRuntimeObjectReferenceValue __baseInterface = WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(baseInterface); + if (isComposable) + { + writer.WriteLine(""" + using WindowsRuntimeObjectReferenceValue __baseInterface = WindowsRuntimeObjectMarshaller.ConvertToUnmanaged(baseInterface); + void* __innerInterface = default; + """, isMultiline: true); + } + + // For mapped value-type params (DateTime, TimeSpan), emit ABI local + marshaller conversion. + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (!context.AbiTypeShapeResolver.IsMappedAbiValueType(p.Type)) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + string abiType = AbiTypeHelpers.GetMappedAbiTypeName(p.Type); + string marshaller = AbiTypeHelpers.GetMappedMarshallerName(p.Type); + writer.WriteLine($" {abiType} __{raw} = {marshaller}.ConvertToUnmanaged({pname});"); + } + + // For HResultException params, emit ABI local + ExceptionMarshaller conversion. + // (HResult is excluded from IsMappedAbiValueType because it's "treated specially in many + // places", but for activator factory ctor params the marshalling pattern is the same.) + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (!p.Type.IsHResultException()) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.WriteLine($" global::ABI.System.Exception __{raw} = global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged({pname});"); + } + + // Declare InlineArray16 + ArrayPool fallback for non-blittable PassArray params + // (runtime classes, objects, strings). + bool hasNonBlittableArray = false; + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + hasNonBlittableArray = true; + string raw = p.Parameter.Name ?? "param"; + string callName = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.WriteLine(); + writer.WriteLine($$""" + Unsafe.SkipInit(out InlineArray16 __{{raw}}_inlineArray); + nint[] __{{raw}}_arrayFromPool = null; + Span __{{raw}}_span = {{callName}}.Length <= 16 + ? __{{raw}}_inlineArray[..{{callName}}.Length] + : (__{{raw}}_arrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent({{callName}}.Length)); + """, isMultiline: true); + + if (szArr.BaseType.IsString()) + { + writer.WriteLine(); + writer.WriteLine($$""" + Unsafe.SkipInit(out InlineArray16 __{{raw}}_inlineHeaderArray); + HStringHeader[] __{{raw}}_headerArrayFromPool = null; + Span __{{raw}}_headerSpan = {{callName}}.Length <= 16 + ? __{{raw}}_inlineHeaderArray[..{{callName}}.Length] + : (__{{raw}}_headerArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent({{callName}}.Length)); + + Unsafe.SkipInit(out InlineArray16 __{{raw}}_inlinePinnedHandleArray); + nint[] __{{raw}}_pinnedHandleArrayFromPool = null; + Span __{{raw}}_pinnedHandleSpan = {{callName}}.Length <= 16 + ? __{{raw}}_inlinePinnedHandleArray[..{{callName}}.Length] + : (__{{raw}}_pinnedHandleArrayFromPool = global::System.Buffers.ArrayPool.Shared.Rent({{callName}}.Length)); + """, isMultiline: true); + } + } + + writer.WriteLine(" void* __retval = default;"); + + if (hasNonBlittableArray) + { + writer.WriteLine(""" + try + { + """, isMultiline: true); + } + string baseIndent = hasNonBlittableArray ? " " : " "; + + // For System.Type params, pre-marshal to TypeReference (must be declared OUTSIDE the + // fixed() block since the fixed block pins the resulting reference). + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (!p.Type.IsSystemType()) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.WriteLine($"{baseIndent}global::ABI.System.TypeMarshaller.ConvertToUnmanagedUnsafe({pname}, out TypeReference __{raw});"); + } + + // Open ONE combined "fixed(void* _a = ..., _b = ..., ...)" block for ALL pinnable + // params (string, Type, PassArray).. + // which emits a single combined fixed-block for all is_pinnable marshalers. + int fixedNesting = 0; + int pinnableCount = 0; + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (p.Type.IsString() || p.Type.IsSystemType()) + { + pinnableCount++; + } + else if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + pinnableCount++; + } + } + + if (pinnableCount > 0) + { + string indent = baseIndent; + writer.Write($"{indent}fixed(void* "); + bool firstPin = true; + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + bool isStr = p.Type.IsString(); + bool isType = p.Type.IsSystemType(); + bool isArr = cat is ParameterCategory.PassArray or ParameterCategory.FillArray; + + if (!isStr && !isType && !isArr) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + + if (!firstPin) + { + writer.Write(", "); + } + + firstPin = false; + writer.Write($"_{raw} = "); + + if (isType) + { + writer.Write($"__{raw}"); + } + else if (isArr) + { + TypeSignature elemT = ((SzArrayTypeSignature)p.Type).BaseType; + bool isBlittableElem = context.AbiTypeShapeResolver.IsBlittablePrimitive(elemT) || context.AbiTypeShapeResolver.IsBlittableStruct(elemT); + bool isStringElem = elemT.IsString(); + + if (isBlittableElem) + { + writer.Write(pname); + } + else { writer.Write($"__{raw}_span"); } + + if (isStringElem) + { + writer.Write($", _{raw}_inlineHeaderArray = __{raw}_headerSpan"); + } + } + else + { + // string param: pin the input string itself. + writer.Write(pname); + } + } + writer.WriteLine($$""" + ) + {{indent}}{ + """, isMultiline: true); + fixedNesting = 1; + // Inside the block: emit HStringMarshaller.ConvertToUnmanagedUnsafe for each + // string input. The HStringReference local lives stack-only. + string innerIndent = baseIndent + new string(' ', fixedNesting * 4); + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + + if (!p.Type.IsString()) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.WriteLine($"{innerIndent}HStringMarshaller.ConvertToUnmanagedUnsafe((char*)_{raw}, {pname}?.Length, out HStringReference __{raw});"); + } + } + + string callIndent = baseIndent + new string(' ', fixedNesting * 4); + + // Emit CopyToUnmanaged for non-blittable PassArray params. + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + + if (szArr.BaseType.IsString()) + { + writer.WriteLine($$""" + {{callIndent}}HStringArrayMarshaller.ConvertToUnmanagedUnsafe( + {{callIndent}} source: {{pname}}, + {{callIndent}} hstringHeaders: (HStringHeader*) _{{raw}}_inlineHeaderArray, + {{callIndent}} hstrings: __{{raw}}_span, + {{callIndent}} pinnedGCHandles: __{{raw}}_pinnedHandleSpan); + """, isMultiline: true); + } + else + { + string elementProjected = TypedefNameWriter.WriteProjectionType(context, TypeSemanticsFactory.Get(szArr.BaseType)); + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + _ = elementInteropArg; + writer.WriteLine($$""" + {{callIndent}}[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "CopyToUnmanaged")] + {{callIndent}}static extern void CopyToUnmanaged_{{raw}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szArr.BaseType)}}")] object _, ReadOnlySpan<{{elementProjected}}> span, uint length, void** data); + {{callIndent}}CopyToUnmanaged_{{raw}}(null, {{pname}}, (uint){{pname}}.Length, (void**)_{{raw}}); + """, isMultiline: true); + } + } + + writer.Write($"{callIndent}RestrictedErrorInfo.ThrowExceptionForHR((*(delegate* unmanaged[MemberFunction]**)ThisPtr)[{(6 + factoryMethodIndex).ToString(CultureInfo.InvariantCulture)}](ThisPtr"); + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + string raw = p.Parameter.Name ?? "param"; + string pname = CSharpKeywords.IsKeyword(raw) ? "@" + raw : raw; + writer.Write(""" + , + + """, isMultiline: true); + if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + writer.Write($"(uint){pname}.Length, _{raw}"); + continue; + } + + // For enums, cast to underlying type. For bool, cast to byte. For char, cast to ushort. + // For string params, use the marshalled HString from the fixed block. + // For runtime class / object / generic instance params, use __.GetThisPtrUnsafe(). + if (context.AbiTypeShapeResolver.IsEnumType(p.Type)) + { + // No cast needed: function pointer signature uses the projected enum type. + writer.Write(pname); + } + else if (p.Type is CorLibTypeSignature corlibBool && + corlibBool.ElementType == ElementType.Boolean) + { + writer.Write(pname); + } + else if (p.Type is CorLibTypeSignature corlibChar && + corlibChar.ElementType == ElementType.Char) + { + writer.Write(pname); + } + else if (p.Type.IsString()) + { + writer.Write($"__{raw}.HString"); + } + else if (p.Type.IsSystemType()) + { + writer.Write($"__{raw}.ConvertToUnmanagedUnsafe()"); + } + else if (context.AbiTypeShapeResolver.IsRuntimeClassOrInterface(p.Type) || p.Type.IsObject() || p.Type.IsGenericInstance()) + { + writer.Write($"__{raw}.GetThisPtrUnsafe()"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(p.Type)) + { + writer.Write($"__{raw}"); + } + else if (p.Type.IsHResultException()) + { + writer.Write($"__{raw}"); + } + else + { + writer.Write(pname); + } + } + + if (isComposable) + { + // Pass __baseInterface.GetThisPtrUnsafe() and &__innerInterface. + writer.Write(""" + , + __baseInterface.GetThisPtrUnsafe(), + &__innerInterface + """, isMultiline: true); + } + writer.WriteLine(""" + , + &__retval)); + """, isMultiline: true); + if (isComposable) + { + writer.WriteLine($"{callIndent}innerInterface = __innerInterface;"); + } + + writer.WriteLine($"{callIndent}retval = __retval;"); + + // Close fixed blocks (innermost first). + for (int i = fixedNesting - 1; i >= 0; i--) + { + string indent = baseIndent + new string(' ', i * 4); + writer.WriteLine($"{indent}}}"); + } + + // Close try and emit finally with cleanup for non-blittable PassArray params. + if (hasNonBlittableArray) + { + writer.WriteLine(""" + } + finally + { + """, isMultiline: true); + for (int i = 0; i < paramCount; i++) + { + ParameterInfo p = sig.Parameters[i]; + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is not (ParameterCategory.PassArray or ParameterCategory.FillArray)) + { + continue; + } + + if (p.Type is not SzArrayTypeSignature szArr) + { + continue; + } + + if (context.AbiTypeShapeResolver.IsBlittablePrimitive(szArr.BaseType) || context.AbiTypeShapeResolver.IsBlittableStruct(szArr.BaseType)) + { + continue; + } + + string raw = p.Parameter.Name ?? "param"; + + if (szArr.BaseType.IsString()) + { + writer.WriteLine(); + writer.WriteLine($$""" + HStringArrayMarshaller.Dispose(__{{raw}}_pinnedHandleSpan); + + if (__{{raw}}_pinnedHandleArrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{raw}}_pinnedHandleArrayFromPool); + } + + if (__{{raw}}_headerArrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{raw}}_headerArrayFromPool); + } + """, isMultiline: true); + } + else + { + string elementInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(szArr.BaseType, TypedefNameType.Projected); + _ = elementInteropArg; + writer.WriteLine(); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Dispose")] + static extern void Dispose_{{raw}}([UnsafeAccessorType("{{ArrayElementEncoder.GetArrayMarshallerInteropPath(szArr.BaseType)}}")] object _, uint length, void** data); + + fixed(void* _{{raw}} = __{{raw}}_span) + { + Dispose_{{raw}}(null, (uint) __{{raw}}_span.Length, (void**)_{{raw}}); + } + """, isMultiline: true); + } + writer.WriteLine(); + writer.WriteLine($$""" + if (__{{raw}}_arrayFromPool is not null) + { + global::System.Buffers.ArrayPool.Shared.Return(__{{raw}}_arrayFromPool); + } + """, isMultiline: true); + } + writer.WriteLine(" }"); + } + + writer.WriteLine(""" + } + } + """, isMultiline: true); + } + + /// + /// Returns the IID expression for the class's default interface. + /// + private static string GetDefaultInterfaceIid(ProjectionEmitContext context, TypeDefinition classType) + { + ITypeDefOrRef? defaultIface = classType.GetDefaultInterface(); + + if (defaultIface is null) + { + return "default(global::System.Guid)"; + } + + string result = ObjRefNameGenerator.WriteIidExpression(context, defaultIface); + return result; + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.cs new file mode 100644 index 0000000000..04b8906c79 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the constructor surface (RCW base-chaining ctors, factory-driven activatable ctors, +/// composable ctors) for projected runtime classes. +/// +/// +/// The implementation is split across several partial files: +/// +/// ConstructorFactory.AttributedTypes.cs - factory-driven activatable + statics constructors. +/// ConstructorFactory.FactoryCallbacks.cs - per-factory args struct + callback class emission. +/// ConstructorFactory.Composable.cs - composable (derivable) class constructors. +/// +/// +internal static partial class ConstructorFactory +{ + /// + /// Reads the [MarshalingBehaviorAttribute] on the class and returns the corresponding + /// CreateObjectReferenceMarshalingType.* expression. + /// + internal static string GetMarshalingTypeName(TypeDefinition classType) + { + for (int i = 0; i < classType.CustomAttributes.Count; i++) + { + CustomAttribute attr = classType.CustomAttributes[i]; + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + + if (attrType is null) + { + continue; + } + + if (attrType.Namespace?.Value != WindowsFoundationMetadata || + attrType.Name?.Value != "MarshalingBehaviorAttribute") + { + 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 int v) + { + return v switch + { + 2 => "CreateObjectReferenceMarshalingType.Agile", + 3 => "CreateObjectReferenceMarshalingType.Standard", + _ => "CreateObjectReferenceMarshalingType.Unknown", + }; + } + } + } + return "CreateObjectReferenceMarshalingType.Unknown"; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.CustomAttributes.cs b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs similarity index 52% rename from src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.CustomAttributes.cs rename to src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs index b18f5bd857..49270e7290 100644 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.CustomAttributes.cs +++ b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs @@ -1,63 +1,79 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Globalization; using System.Text; +using AsmResolver; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; -namespace WindowsRuntime.ProjectionGenerator.Writer; +namespace WindowsRuntime.ProjectionWriter.Factories; /// -/// Custom attribute carry-over and platform attribute helpers. Mirrors C++ functions -/// in code_writers.h for write_custom_attributes, write_custom_attribute_args, -/// write_platform_attribute, get_platform, etc. +/// Custom attribute carry-over and platform attribute helpers. /// -internal static partial class CodeWriters +internal static class CustomAttributeFactory { /// - /// Mirrors C++ write_custom_attribute_args. Returns the formatted argument strings. + /// Returns the formatted argument list for emitting as a C# attribute. /// - public static List WriteCustomAttributeArgs(TypeWriter w, CustomAttribute attribute) + /// The custom attribute to format. + /// A list of pre-formatted positional + named argument strings (in order). + public static List WriteCustomAttributeArgs(CustomAttribute attribute) { - List result = new(); - if (attribute.Signature is null) { return result; } + List result = []; + + if (attribute.Signature is null) + { + return result; + } // Detect AttributeUsage which takes an AttributeTargets enum ITypeDefOrRef? attrType = attribute.Constructor?.DeclaringType; - bool isAttributeUsage = attrType?.Name == "AttributeUsageAttribute" || - attrType?.Name == "AttributeUsage"; + bool isAttributeUsage = attrType?.Name?.Value is "AttributeUsageAttribute" or "AttributeUsage"; for (int i = 0; i < attribute.Signature.FixedArguments.Count; i++) { CustomAttributeArgument arg = attribute.Signature.FixedArguments[i]; uint? targetsValue = null; + if (isAttributeUsage && i == 0) { - if (arg.Element is uint u) { targetsValue = u; } - else if (arg.Element is int s) { targetsValue = unchecked((uint)s); } + if (arg.Element is uint u) + { + targetsValue = u; + } + else if (arg.Element is int s) + { + targetsValue = unchecked((uint)s); + } } + if (targetsValue is uint tv) { result.Add(FormatAttributeTargets(tv)); } else { - result.Add(FormatCustomAttributeArg(w, arg)); + result.Add(FormatCustomAttributeArg(arg)); } } for (int i = 0; i < attribute.Signature.NamedArguments.Count; i++) { CustomAttributeNamedArgument named = attribute.Signature.NamedArguments[i]; - result.Add(named.MemberName?.Value + " = " + FormatCustomAttributeArg(w, named.Argument)); + result.Add(named.MemberName?.Value + " = " + FormatCustomAttributeArg(named.Argument)); } return result; } /// /// Formats an AttributeTargets uint value as a bitwise OR of global::System.AttributeTargets.X. - /// Mirrors the C++ AttributeTargets handling in write_custom_attribute_args. /// private static string FormatAttributeTargets(uint value) { @@ -65,10 +81,11 @@ private static string FormatAttributeTargets(uint value) { return "global::System.AttributeTargets.All"; } + // Map each bit to its corresponding enum name. Includes WinMD-specific values // that map to the same .NET enum (e.g., RuntimeClass=512 -> Class, ApiContract=8192 -> Struct). (uint Bit, string Name)[] entries = - { + [ (1, "Delegate"), (2, "Enum"), (4, "Event"), @@ -81,8 +98,8 @@ private static string FormatAttributeTargets(uint value) (1024, "Struct"), (2048, "All"), // InterfaceImpl - not directly representable, use All (8192, "Struct"), // ApiContract -> Struct - }; - List values = new(); + ]; + List values = []; foreach ((uint bit, string name) in entries) { if ((value & bit) != 0) @@ -90,14 +107,16 @@ private static string FormatAttributeTargets(uint value) values.Add("global::System.AttributeTargets." + name); } } + if (values.Count == 0) { return "global::System.AttributeTargets.All"; } + return string.Join(" | ", values); } - private static string FormatCustomAttributeArg(TypeWriter w, CustomAttributeArgument arg) + private static string FormatCustomAttributeArg(CustomAttributeArgument arg) { // The arg can hold scalar, type, enum or string values. object? element = arg.Element; @@ -105,7 +124,7 @@ private static string FormatCustomAttributeArg(TypeWriter w, CustomAttributeArgu { null => "null", string s => "@\"" + EscapeVerbatimString(s) + "\"", - AsmResolver.Utf8String us => "@\"" + EscapeVerbatimString(us.Value) + "\"", + Utf8String us => "@\"" + EscapeVerbatimString(us.Value) + "\"", bool b => b ? "true" : "false", byte by => by.ToString(CultureInfo.InvariantCulture), sbyte sb => sb.ToString(CultureInfo.InvariantCulture), @@ -118,11 +137,10 @@ private static string FormatCustomAttributeArg(TypeWriter w, CustomAttributeArgu float f => f.ToString("R", CultureInfo.InvariantCulture) + "f", double d => d.ToString("R", CultureInfo.InvariantCulture), char c => "'" + c + "'", - // Always prepend 'global::' to typeof() arguments. The C++ cswinrt tool does this for the - // same reason: when the generated file's namespace context happens to contain a 'Windows' - // sub-namespace (e.g. 'TestComponentCSharp.Windows.*'), an unqualified 'Windows.Foundation.X' - // would resolve to 'TestComponentCSharp.Windows.Foundation.X' first under C# name lookup - // and fail with CS0234. The 'global::' prefix forces fully-qualified resolution. + // Always prepend 'global::' to typeof() arguments: when the generated file's namespace + // context happens to contain a 'Windows' sub-namespace (e.g. 'TestComponentCSharp.Windows.*'), + // an unqualified 'Windows.Foundation.X' would resolve to 'TestComponentCSharp.Windows.Foundation.X' + // first under C# name lookup and fail with CS0234. 'global::' forces fully-qualified resolution. TypeSignature ts when ts.FullName is { Length: > 0 } fn => "typeof(global::" + fn + ")", TypeSignature => "typeof(object)", _ => element.ToString() ?? "null" @@ -131,16 +149,17 @@ private static string FormatCustomAttributeArg(TypeWriter w, CustomAttributeArgu /// /// Escapes a string for use inside a C# verbatim string literal (@"..."). - /// Mirrors C++ write_custom_attribute_args string emission (code_writers.h:2401-2427): - /// the WinMD attribute string value carries source-level escape sequences (e.g. \" - /// for an embedded quote). The C++ tool un-escapes these before emitting a verbatim string, + /// + /// + /// The WinMD attribute string value carries source-level escape sequences (e.g. \" + /// for an embedded quote). the original code un-escapes these before emitting a verbatim string, /// so a WinMD value of \"quotes\" becomes the verbatim source text ""quotes"" /// (which decodes to "quotes" at runtime). /// Logic: /// - \ followed by \ / ' / ": drop the backslash, keep the char. /// - \ followed by anything else: keep both \ and the char. /// - Each emitted " is doubled ("") per verbatim-string escape rules. - /// + /// private static string EscapeVerbatimString(string s) { StringBuilder sb = new(s.Length); @@ -152,33 +171,48 @@ private static string EscapeVerbatimString(string s) prevEscape = true; continue; } + if (prevEscape && c != '\\' && c != '\'' && c != '"') { - sb.Append('\\'); + _ = sb.Append('\\'); } + prevEscape = false; - sb.Append(c); - if (c == '"') { sb.Append('"'); } + _ = sb.Append(c); + + if (c == '"') + { + _ = sb.Append('"'); + } } - if (prevEscape) { sb.Append('\\'); } + + if (prevEscape) + { + _ = sb.Append('\\'); + } + return sb.ToString(); } /// - /// Mirrors C++ get_platform(writer&, CustomAttribute): returns the formatted - /// SupportedOSPlatform string ("WindowsX.Y.Z.0") for a [ContractVersion] attribute, - /// or empty if no platform mapping exists. Honors writer's - /// state to deduplicate platforms within a single class scope (mirrors C++ - /// _check_platform / _platform behavior in code_writers.h:2515-2525). + /// Returns the SupportedOSPlatform string ("WindowsX.Y.Z.0") for a + /// [ContractVersion] attribute, or empty if no platform mapping exists. Honors the + /// active context's mode flag to deduplicate + /// platforms within a single class scope. /// - private static string GetPlatform(TypeWriter w, CustomAttribute attribute) + /// The active emit context. + /// The [ContractVersion] attribute to inspect. + /// The platform string (with surrounding quotes), or an empty string. + private static string GetPlatform(ProjectionEmitContext context, CustomAttribute attribute) { if (attribute.Signature is null || attribute.Signature.FixedArguments.Count < 2) { return string.Empty; } + CustomAttributeArgument arg0 = attribute.Signature.FixedArguments[0]; string contractName; + if (arg0.Element is TypeSignature ts && ts.FullName is { } fn) { contractName = fn; @@ -191,7 +225,11 @@ private static string GetPlatform(TypeWriter w, CustomAttribute attribute) { // AsmResolver returns Utf8String for string custom-attribute args. contractName = arg0.Element.ToString() ?? string.Empty; - if (contractName.Length == 0) { return string.Empty; } + + if (contractName.Length == 0) + { + return string.Empty; + } } else { @@ -209,50 +247,65 @@ private static string GetPlatform(TypeWriter w, CustomAttribute attribute) int contractVersion = (int)(versionRaw >> 16); string platform = ContractPlatforms.GetPlatform(contractName, contractVersion); - if (string.IsNullOrEmpty(platform)) { return string.Empty; } - if (w.CheckPlatform) + + if (string.IsNullOrEmpty(platform)) + { + return string.Empty; + } + + if (context.CheckPlatform) { // Suppress when this platform is <= the previously seen platform for the class. - if (string.CompareOrdinal(platform, w.Platform) <= 0) + if (string.CompareOrdinal(platform, context.Platform) <= 0) { return string.Empty; } - // Only seed _platform on first non-empty observation (matches C++ behavior: - // higher platforms emit but don't update _platform). - if (w.Platform.Length == 0) - { - w.Platform = platform; - } + + // Only seed Platform on first non-empty observation: higher platforms emit but don't update Platform. + context.SeedPlatform(platform); } + return "\"Windows" + platform + "\""; } /// - /// Mirrors C++ write_platform_attribute: emits [SupportedOSPlatform("WindowsX.Y.Z.0")] - /// for a [ContractVersion] attribute. Only writes for reference projection. + /// Writes the [SupportedOSPlatform] attribute for a [ContractVersion] attribute + /// on . Only writes for reference projection. /// - public static void WritePlatformAttribute(TypeWriter w, IHasCustomAttribute member) + /// The writer to emit to. + /// The active emit context. + /// The member to inspect for [ContractVersion]. + public static void WritePlatformAttribute(IndentedTextWriter writer, ProjectionEmitContext context, IHasCustomAttribute member) { - if (!w.Settings.ReferenceProjection) { return; } + if (!context.Settings.ReferenceProjection) + { + return; + } + for (int i = 0; i < member.CustomAttributes.Count; i++) { CustomAttribute attr = member.CustomAttributes[i]; ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; - if (attrType is null) { continue; } + + if (attrType is null) + { + continue; + } + string name = attrType.Name?.Value ?? string.Empty; - // Strip 'Attribute' suffix - if (name.EndsWith("Attribute", System.StringComparison.Ordinal)) + + if (name.EndsWith("Attribute", StringComparison.Ordinal)) { - name = name.Substring(0, name.Length - "Attribute".Length); + name = name[..^"Attribute".Length]; } + if (name == "ContractVersion" && attr.Signature?.FixedArguments.Count == 2) { - string platform = GetPlatform(w, attr); + string platform = GetPlatform(context, attr); + if (!string.IsNullOrEmpty(platform)) { - w.Write("[global::System.Runtime.Versioning.SupportedOSPlatform("); - w.Write(platform); - w.Write(")]\n"); + writer.WriteLine($"[global::System.Runtime.Versioning.SupportedOSPlatform({platform})]"); return; } } @@ -260,59 +313,92 @@ public static void WritePlatformAttribute(TypeWriter w, IHasCustomAttribute memb } /// - /// Mirrors C++ write_custom_attributes: carries selected custom attributes - /// to the projection (e.g., [Obsolete], [Deprecated], [SupportedOSPlatform]). + /// Convenience overload of + /// that leases an from , + /// emits the [SupportedOSPlatform] attribute (if any) into it, and returns the + /// resulting string. Returns the empty string when no attribute is emitted. + /// + /// The active emit context. + /// The member to inspect for [ContractVersion]. + /// The emitted attribute, or when none. + public static string WritePlatformAttribute(ProjectionEmitContext context, IHasCustomAttribute member) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WritePlatformAttribute(writer, context, member); + return writer.ToString(); + } + + /// + /// Writes any custom attributes (e.g. [Obsolete], [Deprecated], + /// [SupportedOSPlatform]) carried over from to the projection. /// - public static void WriteCustomAttributes(TypeWriter w, IHasCustomAttribute member, bool enablePlatformAttrib) + /// The writer to emit to. + /// The active emit context. + /// The metadata member whose custom attributes to emit. + /// Whether to also emit a [SupportedOSPlatform] attribute synthesized from any [ContractVersion]. + public static void WriteCustomAttributes(IndentedTextWriter writer, ProjectionEmitContext context, IHasCustomAttribute member, bool enablePlatformAttrib) { - Dictionary> attributes = new(System.StringComparer.Ordinal); + Dictionary> attributes = []; bool allowMultiple = false; for (int i = 0; i < member.CustomAttributes.Count; i++) { CustomAttribute attr = member.CustomAttributes[i]; ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; - if (attrType is null) { continue; } - string ns = attrType.Namespace?.Value ?? string.Empty; - string name = attrType.Name?.Value ?? string.Empty; - // Strip 'Attribute' suffix - string strippedName = name.EndsWith("Attribute", System.StringComparison.Ordinal) - ? name.Substring(0, name.Length - "Attribute".Length) + + if (attrType is null) + { + continue; + } + + (string ns, string name) = attrType.Names(); + string strippedName = name.EndsWith("Attribute", StringComparison.Ordinal) + ? name[..^"Attribute".Length] : name; // Skip attributes handled separately - if (strippedName is "GCPressure" or "Guid" or "Flags" or "ProjectionInternal") { continue; } + if (strippedName is "GCPressure" or "Guid" or "Flags" or "ProjectionInternal") + { + continue; + } string fullAttrName = strippedName == "AttributeUsage" ? "System.AttributeUsage" : ns + "." + strippedName; - List args = WriteCustomAttributeArgs(w, attr); + List args = WriteCustomAttributeArgs(attr); - if (w.Settings.ReferenceProjection && enablePlatformAttrib && strippedName == "ContractVersion" && attr.Signature?.FixedArguments.Count == 2) + if (context.Settings.ReferenceProjection && enablePlatformAttrib && strippedName == "ContractVersion" && attr.Signature?.FixedArguments.Count == 2) { - string platform = GetPlatform(w, attr); + string platform = GetPlatform(context, attr); + if (!string.IsNullOrEmpty(platform)) { if (!attributes.TryGetValue("System.Runtime.Versioning.SupportedOSPlatform", out List? list)) { - list = new List(); + list = []; attributes["System.Runtime.Versioning.SupportedOSPlatform"] = list; } + list.Add(platform); } } // Skip metadata attributes without a projection - if (ns == "Windows.Foundation.Metadata") + if (ns == WindowsFoundationMetadata) { if (strippedName == "AllowMultiple") { allowMultiple = true; } + if (strippedName == "ContractVersion") { - if (!w.Settings.ReferenceProjection) { continue; } + if (!context.Settings.ReferenceProjection) + { + continue; + } } else if (strippedName is not ("DefaultOverload" or "Overload" or "AttributeUsage" or "Experimental")) { @@ -331,25 +417,37 @@ public static void WriteCustomAttributes(TypeWriter w, IHasCustomAttribute membe foreach (KeyValuePair> kv in attributes) { - w.Write("[global::"); - w.Write(kv.Key); + writer.Write($"[global::{kv.Key}"); + if (kv.Value.Count > 0) { - w.Write("("); + writer.Write("("); for (int i = 0; i < kv.Value.Count; i++) { - if (i > 0) { w.Write(", "); } - w.Write(kv.Value[i]); + if (i > 0) + { + writer.Write(", "); + } + + writer.Write(kv.Value[i]); } - w.Write(")"); + writer.Write(")"); } - w.Write("]\n"); + + writer.WriteLine("]"); } } - /// Mirrors C++ write_type_custom_attributes. - public static void WriteTypeCustomAttributes(TypeWriter w, TypeDefinition type, bool enablePlatformAttrib) + /// + /// Writes the type-level custom attributes for . + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + /// Whether to also emit a [SupportedOSPlatform] attribute synthesized from any [ContractVersion]. + public static void WriteTypeCustomAttributes(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool enablePlatformAttrib) { - WriteCustomAttributes(w, type, enablePlatformAttrib); + WriteCustomAttributes(writer, context, type, enablePlatformAttrib); } + } diff --git a/src/WinRT.Projection.Writer/Factories/EventTableFactory.cs b/src/WinRT.Projection.Writer/Factories/EventTableFactory.cs new file mode 100644 index 0000000000..f4c459cedd --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/EventTableFactory.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the per-instance event-source storage field and the ABI add/remove handlers for runtime events on projected interfaces. +/// +internal static class EventTableFactory +{ + /// + /// Emits the per-event ConditionalWeakTable<TInterface, EventRegistrationTokenTable<THandler>> + /// backing field property. 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. + /// + internal static void EmitEventTableField(IndentedTextWriter writer, ProjectionEmitContext context, EventDefinition evt, string ifaceFullName) + { + string evName = evt.Name?.Value ?? "Event"; + string evtType = TypedefNameWriter.WriteEventType(context, evt); + + writer.WriteLine(); + writer.WriteLine($$""" + private static ConditionalWeakTable<{{ifaceFullName}}, EventRegistrationTokenTable<{{evtType}}>> _{{evName}} + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + [MethodImpl(MethodImplOptions.NoInlining)] + static ConditionalWeakTable<{{ifaceFullName}}, EventRegistrationTokenTable<{{evtType}}>> MakeTable() + { + _ = global::System.Threading.Interlocked.CompareExchange(ref field, [], null); + + return global::System.Threading.Volatile.Read(in field); + } + + return global::System.Threading.Volatile.Read(in field) ?? MakeTable(); + } + } + """, isMultiline: true); + } + + /// + /// Emits the body of the Do_Abi_add_<EventName>_N method. + /// + internal static void EmitDoAbiAddEvent(IndentedTextWriter writer, ProjectionEmitContext context, EventDefinition evt, MethodSignatureInfo 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.Parameters.Count > 0 ? (sig.Parameters[^1].Parameter.Name ?? "handler") : "handler"; + string handlerRef = CSharpKeywords.IsKeyword(handlerRawName) ? "@" + handlerRawName : handlerRawName; + + // The cookie/token return parameter takes the metadata return param name (matches truth). + string cookieName = AbiTypeHelpers.GetReturnParamName(sig); + + TypeSignature evtTypeSig = evt.EventType!.ToTypeSignature(false); + bool isGeneric = evtTypeSig is GenericInstanceTypeSignature; + + writer.WriteLine(); + writer.WriteLine($$""" + { + *{{cookieName}} = default; + try + { + var __this = ComInterfaceDispatch.GetInstance<{{ifaceFullName}}>((ComInterfaceDispatch*)thisPtr); + """, isMultiline: true); + + if (isGeneric) + { + string interopTypeName = InteropTypeNameWriter.EncodeInteropTypeName(evtTypeSig, TypedefNameType.ABI) + ", WinRT.Interop"; + string projectedTypeName = MethodFactory.WriteProjectedSignature(context, evtTypeSig, false); + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "ConvertToManaged")] + static extern {{projectedTypeName}} ConvertToManaged([UnsafeAccessorType("{{interopTypeName}}")] object _, void* value); + var __handler = ConvertToManaged(null, {{handlerRef}}); + """, isMultiline: true); + } + else + { + writer.Write(" var __handler = "); + TypedefNameWriter.WriteTypeName(writer, context, TypeSemanticsFactory.Get(evtTypeSig), TypedefNameType.ABI, false); + writer.WriteLine($"Marshaller.ConvertToManaged({handlerRef});"); + } + + writer.WriteLine($$""" + *{{cookieName}} = _{{evName}}.GetOrCreateValue(__this).AddEventHandler(__handler); + __this.{{evName}} += __handler; + return 0; + } + catch (Exception __exception__) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__); + } + } + """, isMultiline: true); + } + + /// + /// Emits the body of the Do_Abi_remove_<EventName>_N method. + /// + internal static void EmitDoAbiRemoveEvent(IndentedTextWriter writer, ProjectionEmitContext context, EventDefinition evt, MethodSignatureInfo sig, string ifaceFullName) + { + string evName = evt.Name?.Value ?? "Event"; + string tokenRawName = sig.Parameters.Count > 0 ? (sig.Parameters[^1].Parameter.Name ?? "token") : "token"; + string tokenRef = CSharpKeywords.IsKeyword(tokenRawName) ? "@" + tokenRawName : tokenRawName; + + writer.WriteLine(); + writer.WriteLine($$""" + { + try + { + var __this = ComInterfaceDispatch.GetInstance<{{ifaceFullName}}>((ComInterfaceDispatch*)thisPtr); + if (__this is not null && _{{evName}}.TryGetValue(__this, out var __table) && __table.RemoveEventHandler({{tokenRef}}, out var __handler)) + { + __this.{{evName}} -= __handler; + } + return 0; + } + catch (Exception __exception__) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(__exception__); + } + } + """, isMultiline: true); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs new file mode 100644 index 0000000000..dd4b9f19db --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs @@ -0,0 +1,558 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Interface, class, and ABI emission helpers. +/// +internal static class InterfaceFactory +{ + /// + /// Writes the [Guid("...")] attribute for a type. + /// + public static void WriteGuidAttribute(IndentedTextWriter writer, TypeDefinition type) + { + bool fullyQualify = type.Namespace == WindowsFoundationMetadata; + writer.Write($"[{(fullyQualify ? "global::System.Runtime.InteropServices.Guid" : "Guid")}(\""); + IidExpressionGenerator.WriteGuid(writer, type, false); + writer.Write("\")]"); + } + + /// + /// Writes a class or interface inheritance clause: " : Base, Iface1, Iface2<T>". + /// + public static void WriteTypeInheritance(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool includeExclusiveInterface, bool includeWindowsRuntimeObject) + { + string delimiter = " : "; + + // Check the base type. If the class extends another runtime class (not System.Object), + // emit the projected base type name. WindowsRuntime.WindowsRuntimeObject is a managed + // type defined in WinRT.Runtime and is never referenced as a base type in any .winmd, so + // there is no need to check for it here. + bool hasNonObjectBase = false; + + if (type.BaseType is not null) + { + string? baseNs = type.BaseType.Namespace?.Value; + string? baseName = type.BaseType.Name?.Value; + hasNonObjectBase = !(baseNs == "System" && baseName == "Object"); + } + + if (hasNonObjectBase) + { + writer.Write(delimiter); + // Same-namespace types stay unqualified (e.g. 'AppointmentActionEntity : ActionEntity'): + // only emit 'global::' when the base class lives in a different namespace. + ITypeDefOrRef baseType = type.BaseType!; + (string ns, string name) = baseType.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + if (!string.IsNullOrEmpty(ns) && ns != context.CurrentNamespace) + { + writer.Write($"global::{ns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(name)); + delimiter = ", "; + } + else if (includeWindowsRuntimeObject) + { + writer.Write($"{delimiter}WindowsRuntimeObject"); + delimiter = ", "; + } + + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + bool isOverridable = impl.IsOverridable(); + + // For TypeDef interfaces, check exclusive_to attribute to decide inclusion. + // For TypeRef interfaces, attempt to resolve via the runtime context. + bool isExclusive = false; + + if (impl.Interface is TypeDefinition ifaceTypeDef) + { + isExclusive = TypeCategorization.IsExclusiveTo(ifaceTypeDef); + } + else + { + TypeDefinition? resolved = ClassMembersFactory.ResolveInterface(context.Cache, impl.Interface); + + if (resolved is not null) + { + isExclusive = TypeCategorization.IsExclusiveTo(resolved); + } + } + + if (!(isOverridable || !isExclusive || includeExclusiveInterface)) + { + continue; + } + + writer.Write(delimiter); + delimiter = ", "; + + // Emit the interface name (CCW) with mapped-type remapping. + WriteInterfaceTypeName(writer, context, impl.Interface); + + if (includeWindowsRuntimeObject && !context.Settings.ReferenceProjection) + { + writer.Write(", IWindowsRuntimeInterface<"); + WriteInterfaceTypeName(writer, context, impl.Interface); + writer.Write(">"); + } + } + } + + /// + /// Writes the projected name for an interface reference (TypeDefinition, TypeReference, or + /// generic instance), applying mapped-type remapping (e.g., + /// Windows.Foundation.Collections.IMap<K,V> -> System.Collections.Generic.IDictionary<K,V>). + /// + public static void WriteInterfaceTypeName(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + if (ifaceType is TypeDefinition td) + { + TypedefNameWriter.WriteTypedefName(writer, context, td, TypedefNameType.CCW, false); + TypedefNameWriter.WriteTypeParams(writer, td); + } + else if (ifaceType is TypeReference tr) + { + (string ns, string name) = tr.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m1) + { + ns = m1.MappedNamespace; + name = m1.MappedName; + } + + // Only emit the global:: prefix when the namespace doesn't match the current emit + // namespace (mirrors WriteTypedefName behavior -- same-namespace stays unqualified). + if (!string.IsNullOrEmpty(ns) && ns != context.CurrentNamespace) + { + writer.Write($"global::{ns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(name)); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef gt = gi.GenericType; + (string ns, string name) = gt.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m2) + { + ns = m2.MappedNamespace; + name = m2.MappedName; + } + + if (!string.IsNullOrEmpty(ns) && ns != context.CurrentNamespace) + { + writer.Write($"global::{ns}."); + } + + writer.Write($"{IdentifierEscaping.StripBackticks(name)}<"); + for (int i = 0; i < gi.TypeArguments.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + // Pass forceWriteNamespace=false so type args also respect the current namespace. + TypedefNameWriter.WriteTypeName(writer, context, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, false); + } + writer.Write(">"); + } + } + + /// + /// Returns the projected property type for . + /// + public static string WritePropType(ProjectionEmitContext context, PropertyDefinition prop, bool isSetProperty = false) + => WritePropType(context, prop, null, isSetProperty); + + /// + /// Returns the projected property type for , optionally substituting generic args. + /// + public static string WritePropType(ProjectionEmitContext context, PropertyDefinition prop, GenericContext? genericContext, bool isSetProperty = false) + { + TypeSignature? typeSig = prop.Signature?.ReturnType; + + if (typeSig is null) + { + return "object"; + } + + if (genericContext is not null) + { + typeSig = typeSig.InstantiateGenericTypes(genericContext.Value); + } + + string result = MethodFactory.WriteProjectedSignature(context, typeSig, isSetProperty); + return result; + } + + /// + /// Emits all method, property, and event signatures of an interface. + /// + public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + foreach (MethodDefinition method in type.Methods) + { + if (method.IsSpecial()) + { + continue; + } + + MethodSignatureInfo sig = new(method); + // Only emit Windows.Foundation.Metadata attributes that have a projected form + // (Overload, DefaultOverload, AttributeUsage, Experimental). + WriteMethodCustomAttributes(writer, method); + MethodFactory.WriteProjectionReturnType(writer, context, sig); + writer.Write($" {method.Name?.Value ?? string.Empty}("); + MethodFactory.WriteParameterList(writer, context, sig); + writer.WriteLine(");"); + } + + foreach (PropertyDefinition prop in type.Properties) + { + (MethodDefinition? getter, MethodDefinition? setter) = prop.GetPropertyMethods(); + // Add 'new' when this interface has a setter-only property AND a property of the same + // name exists on a base interface (typically the getter-only counterpart). This hides + // the inherited member. + string newKeyword = (getter is null && setter is not null + && FindPropertyInBaseInterfaces(context.Cache, type, prop.Name?.Value ?? string.Empty)) + ? "new " : string.Empty; + string propType = WritePropType(context, prop); + writer.Write($"{newKeyword}{propType} {prop.Name?.Value ?? string.Empty} {{"); + + if (getter is not null || setter is not null) + { + writer.Write(" get;"); + } + + if (setter is not null) + { + writer.Write(" set;"); + } + + writer.WriteLine(" }"); + } + + foreach (EventDefinition evt in type.Events) + { + writer.Write("event "); + TypedefNameWriter.WriteEventType(writer, context, evt); + writer.WriteLine($" {evt.Name?.Value ?? string.Empty};"); + } + } + + /// + /// Recursively walks the base interfaces of looking for a property + /// with the given . Returns true if any base interface declares + /// a property with that name (used to decide whether a setter-only property in a derived + /// interface needs the new modifier to hide the base getter). + /// + private static bool FindPropertyInBaseInterfaces(MetadataCache cache, TypeDefinition type, string propName) + { + if (string.IsNullOrEmpty(propName)) + { + return false; + } + + HashSet visited = []; + return FindPropertyInBaseInterfacesRecursive(cache, type, propName, visited); + } + + private static bool FindPropertyInBaseInterfacesRecursive(MetadataCache cache, TypeDefinition type, string propName, HashSet visited) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + TypeDefinition? baseIface = ClassMembersFactory.ResolveInterface(cache, impl.Interface); + + if (baseIface is null) + { + continue; + } + + // Skip the original setter-defining interface itself. Also dedupe via the visited set. + if (baseIface == type) + { + continue; + } + + if (!visited.Add(baseIface)) + { + continue; + } + + foreach (PropertyDefinition prop in baseIface.Properties) + { + if ((prop.Name?.Value ?? string.Empty) == propName) + { + return true; + } + } + + if (FindPropertyInBaseInterfacesRecursive(cache, baseIface, propName, visited)) + { + return true; + } + } + return false; + } + + /// + /// Like but returns the base interface where the + /// property was found (or null if not found). + /// + internal static TypeDefinition? FindPropertyInterfaceInBases(MetadataCache cache, TypeDefinition type, string propName) + { + if (string.IsNullOrEmpty(propName)) + { + return null; + } + + HashSet visited = []; + return FindPropertyInterfaceInBasesRecursive(cache, type, propName, visited); + } + + private static TypeDefinition? FindPropertyInterfaceInBasesRecursive(MetadataCache cache, TypeDefinition type, string propName, HashSet visited) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + TypeDefinition? baseIface = ClassMembersFactory.ResolveInterface(cache, impl.Interface); + + if (baseIface is null) + { + continue; + } + + if (baseIface == type) + { + continue; + } + + if (!visited.Add(baseIface)) + { + continue; + } + + foreach (PropertyDefinition prop in baseIface.Properties) + { + if ((prop.Name?.Value ?? string.Empty) == propName) + { + return baseIface; + } + } + TypeDefinition? deeper = FindPropertyInterfaceInBasesRecursive(cache, baseIface, propName, visited); + + if (deeper is not null) + { + return deeper; + } + } + return null; + } + + /// + /// Emits the projected custom attributes for an interface method (filtered for the projected + /// attributes: Overload, DefaultOverload, Experimental). + /// + private static void WriteMethodCustomAttributes(IndentedTextWriter writer, MethodDefinition method) + { + foreach (CustomAttribute attr in method.CustomAttributes) + { + ITypeDefOrRef? attrType = attr.Constructor?.DeclaringType; + + if (attrType is null) + { + continue; + } + + (string ns, string nm) = attrType.Names(); + + if (ns != WindowsFoundationMetadata) + { + continue; + } + + string baseName = nm.EndsWith("Attribute", StringComparison.Ordinal) ? nm[..^"Attribute".Length] : nm; + + if (baseName is not ("Overload" or "DefaultOverload" or "Experimental")) + { + continue; + } + + writer.Write($"[global::Windows.Foundation.Metadata.{baseName}"); + // Args: only handle string args (sufficient for [Overload(@"X")]). [DefaultOverload] has none. + if (attr.Signature is not null && attr.Signature.FixedArguments.Count > 0) + { + writer.Write("("); + for (int i = 0; i < attr.Signature.FixedArguments.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + object? val = attr.Signature.FixedArguments[i].Element; + + if (val is Utf8String s) + { + writer.Write($"@\"{s.Value}\""); + } + else if (val is string ss) + { + writer.Write($"@\"{ss}\""); + } + else + { + writer.Write(val?.ToString() ?? string.Empty); + } + } + writer.Write(")"); + } + + writer.WriteLine("]"); + } + } + + /// + /// Writes a projected interface declaration. + /// + public static void WriteInterface(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // [Default] and overridable interfaces aren't used in the projection. Skip them unless + // public_exclusiveto is set (or in reference projection or component mode). + if (!context.Settings.ReferenceProjection && + !context.Settings.Component && + TypeCategorization.IsExclusiveTo(type) && + !context.Settings.PublicExclusiveTo && + !IsDefaultOrOverridableInterfaceTypedef(context.Cache, type)) + { + return; + } + + if (context.Settings.Component && !TypeCategorization.IsExclusiveTo(type)) + { + return; + } + + writer.WriteLine(); + MetadataAttributeFactory.WriteWinRTMetadataAttribute(writer, type, context.Cache); + WriteGuidAttribute(writer, type); + writer.WriteLine(); + CustomAttributeFactory.WriteTypeCustomAttributes(writer, context, type, false); + + bool isInternal = (TypeCategorization.IsExclusiveTo(type) && !context.Settings.PublicExclusiveTo) || + TypeCategorization.IsProjectionInternal(type); + writer.Write($"{(isInternal ? "internal" : "public")} interface "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.CCW, false); + TypedefNameWriter.WriteTypeParams(writer, type); + WriteTypeInheritance(writer, context, type, false, false); + writer.WriteLine(); + using (writer.WriteBlock()) + { + WriteInterfaceMemberSignatures(writer, context, type); + } + } + + /// Returns true if the given exclusive interface is referenced as a [Default] or + /// [Overridable] interface impl on the class it's exclusive to. + private static bool IsDefaultOrOverridableInterfaceTypedef(MetadataCache cache, TypeDefinition iface) + { + if (!TypeCategorization.IsExclusiveTo(iface)) + { + return false; + } + + TypeDefinition? classType = AbiTypeHelpers.GetExclusiveToType(cache, iface); + + if (classType is null) + { + return false; + } + + foreach (InterfaceImplementation impl in classType.Interfaces) + { + if (!impl.IsDefaultInterface() && !impl.IsOverridable()) + { + continue; + } + + ITypeDefOrRef? implRef = impl.Interface; + + if (implRef is null) + { + continue; + } + + TypeDefinition? implDef = ResolveInterfaceTypeDefForExclusiveCheck(cache, implRef); + + if (implDef is not null && implDef == iface) + { + return true; + } + } + return false; + } + + private static TypeDefinition? ResolveInterfaceTypeDefForExclusiveCheck(MetadataCache cache, ITypeDefOrRef ifaceRef) + { + if (ifaceRef is TypeDefinition td) + { + return td; + } + + if (ifaceRef is TypeReference tr) + { + (string ns, string nm) = tr.Names(); + return cache.Find(ns + "." + nm); + } + + if (ifaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef? gen = gi.GenericType; + return gen is null ? null : ResolveInterfaceTypeDefForExclusiveCheck(cache, gen); + } + + return null; + } +} diff --git a/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs b/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs new file mode 100644 index 0000000000..e904a4f8c5 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/MappedInterfaceStubFactory.cs @@ -0,0 +1,412 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits stub members ('=> throw null!') for well-known C# interfaces that come from mapped +/// WinRT interfaces (IClosable -> IDisposable, IMap`2 -> IDictionary<K,V>, etc.). The +/// runtime adapter actually services these at runtime via IDynamicInterfaceCastable, but the +/// C# compiler still requires the class to declare the members. +/// +internal static class MappedInterfaceStubFactory +{ + /// + /// Returns true if the WinRT interface (by namespace+name) is a mapped interface that + /// requires emitting C#-interface stub members on the implementing class. + /// + public static bool IsMappedInterfaceRequiringStubs(string ifaceNs, string ifaceName) + { + if (MappedTypes.Get(ifaceNs, ifaceName) is not { HasCustomMembersOutput: true }) + { + return false; + } + + return ifaceName switch + { + "IClosable" => true, + "IIterable`1" or "IIterator`1" => true, + "IMap`2" or "IMapView`2" => true, + "IVector`1" or "IVectorView`1" => true, + "IBindableIterable" or "IBindableIterator" or "IBindableVector" => true, + "INotifyDataErrorInfo" => true, + _ => false, + }; + } + + /// + /// Emits the C# interface stub members for the given WinRT interface that maps to a known + /// .NET interface. + /// + /// The writer. + /// The active emit context. + /// The (possibly substituted) generic instance signature for the interface, or null if non-generic. + /// The WinRT interface name (e.g. "IMap`2"). + /// The name of the lazy _objRef_* field for the interface on the class. + public static void WriteMappedInterfaceStubs(IndentedTextWriter writer, ProjectionEmitContext context, GenericInstanceTypeSignature? instance, string ifaceName, string objRefName) + { + // Resolve type arguments from the (substituted) generic instance signature, if any. + List typeArgs = []; + List typeArgSigs = []; + + if (instance is not null) + { + foreach (TypeSignature arg in instance.TypeArguments) + { + typeArgs.Add(TypeSemanticsFactory.Get(arg)); + typeArgSigs.Add(arg); + } + } + + switch (ifaceName) + { + case "IClosable": + EmitDisposable(writer, objRefName); + break; + case "IIterable`1": + EmitGenericEnumerable(writer, context, typeArgs, typeArgSigs, objRefName); + break; + case "IIterator`1": + EmitGenericEnumerator(writer, context, typeArgs, typeArgSigs, objRefName); + break; + case "IMap`2": + EmitDictionary(writer, context, typeArgs, typeArgSigs, objRefName); + break; + case "IMapView`2": + EmitReadOnlyDictionary(writer, context, typeArgs, typeArgSigs, objRefName); + break; + case "IVector`1": + EmitList(writer, context, typeArgs, typeArgSigs, objRefName); + break; + case "IVectorView`1": + EmitReadOnlyList(writer, context, typeArgs, typeArgSigs, objRefName); + break; + case "IBindableIterable": + writer.WriteLine(); + writer.WriteLine($"IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => global::ABI.System.Collections.IEnumerableMethods.GetEnumerator({objRefName});"); + break; + case "IBindableIterator": + writer.WriteLine(); + writer.WriteLine($$""" + public bool MoveNext() => global::ABI.System.Collections.IEnumeratorMethods.MoveNext({{objRefName}}); + public void Reset() => throw new NotSupportedException(); + public object Current => global::ABI.System.Collections.IEnumeratorMethods.Current({{objRefName}}); + """, isMultiline: true); + break; + case "IBindableVector": + EmitNonGenericList(writer, objRefName); + break; + case "INotifyDataErrorInfo": + writer.WriteLine(); + writer.WriteLine($$""" + public global::System.Collections.IEnumerable GetErrors(string propertyName) => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.GetErrors({{objRefName}}, propertyName); + public bool HasErrors {get => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.HasErrors({{objRefName}}); } + public event global::System.EventHandler ErrorsChanged + { + add => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.ErrorsChanged(this, {{objRefName}}).Subscribe(value); + remove => global::ABI.System.ComponentModel.INotifyDataErrorInfoMethods.ErrorsChanged(this, {{objRefName}}).Unsubscribe(value); + } + """, isMultiline: true); + break; + } + } + private static void EmitDisposable(IndentedTextWriter writer, string objRefName) + { + writer.WriteLine(); + writer.WriteLine($"public void Dispose() => global::ABI.System.IDisposableMethods.Dispose({objRefName});"); + } + + private static void EmitGenericEnumerable(IndentedTextWriter writer, ProjectionEmitContext context, List args, List argSigs, string objRefName) + { + if (args.Count != 1) + { + return; + } + + string t = WriteTypeNameToString(context, args[0], TypedefNameType.Projected, true); + string elementId = EncodeArgIdentifier(context, args[0]); + string interopTypeArgs = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IEnumerable'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IEnumerableMethods_" + elementId + "_"; + + writer.WriteLine(); + EmitUnsafeAccessor(writer, "GetEnumerator", $"IEnumerator<{t}>", $"{prefix}GetEnumerator", interopType, ""); + + writer.WriteLine(); + writer.WriteLine($"public IEnumerator<{t}> GetEnumerator() => {prefix}GetEnumerator(null, {objRefName});"); + writer.WriteLine("global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();"); + } + + private static void EmitGenericEnumerator(IndentedTextWriter writer, ProjectionEmitContext context, List args, List argSigs, string objRefName) + { + if (args.Count != 1) + { + return; + } + + string t = WriteTypeNameToString(context, args[0], TypedefNameType.Projected, true); + string elementId = EncodeArgIdentifier(context, args[0]); + string interopTypeArgs = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IEnumerator'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IEnumeratorMethods_" + elementId + "_"; + + writer.WriteLine(); + EmitUnsafeAccessor(writer, "Current", t, $"{prefix}Current", interopType, ""); + EmitUnsafeAccessor(writer, "MoveNext", "bool", $"{prefix}MoveNext", interopType, ""); + + writer.WriteLine(); + writer.WriteLine($$""" + public bool MoveNext() => {{prefix}}MoveNext(null, {{objRefName}}); + public void Reset() => throw new NotSupportedException(); + public void Dispose() {} + public {{t}} Current => {{prefix}}Current(null, {{objRefName}}); + object global::System.Collections.IEnumerator.Current => Current!; + """, isMultiline: true); + } + + private static void EmitDictionary(IndentedTextWriter writer, ProjectionEmitContext context, List args, List argSigs, string objRefName) + { + if (args.Count != 2) + { + return; + } + + string k = WriteTypeNameToString(context, args[0], TypedefNameType.Projected, true); + string v = WriteTypeNameToString(context, args[1], TypedefNameType.Projected, true); + // Truth uses two forms for KeyValuePair: + // - 'kv' (unqualified) for plain type usages: parameters, field/return types + // - 'kvNested' (fully qualified) for generic argument usages (inside IEnumerator<>, ICollection<>) + string kv = $"KeyValuePair<{k}, {v}>"; + string kvNested = $"global::System.Collections.Generic.KeyValuePair<{k}, {v}>"; + // Long form (always fully qualified) used for objref field-name computation + // (matches the form WriteClassObjRefDefinitions emits transitively). + string kvLong = kvNested; + string keyId = EncodeArgIdentifier(context, args[0]); + string valId = EncodeArgIdentifier(context, args[1]); + string keyInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string valInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[1], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IDictionary'2<" + keyInteropArg + "|" + valInteropArg + ">Methods, WinRT.Interop"; + string prefix = "IDictionaryMethods_" + keyId + "_" + valId + "_"; + // The IEnumerable> objref name (matches what WriteClassObjRefDefinitions emits transitively). + string enumerableObjRefName = "_objRef_System_Collections_Generic_IEnumerable_" + IidExpressionGenerator.EscapeTypeNameForIdentifier(kvLong, stripGlobal: false) + "_"; + + writer.WriteLine(); + EmitUnsafeAccessor(writer, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, ""); + EmitUnsafeAccessor(writer, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, ""); + EmitUnsafeAccessor(writer, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(writer, "Item", v, $"{prefix}Item", interopType, $", {k} key"); + EmitUnsafeAccessor(writer, "Item", "void", $"{prefix}Item", interopType, $", {k} key, {v} value"); + EmitUnsafeAccessor(writer, "Add", "void", $"{prefix}Add", interopType, $", {k} key, {v} value"); + EmitUnsafeAccessor(writer, "ContainsKey", "bool", $"{prefix}ContainsKey", interopType, $", {k} key"); + EmitUnsafeAccessor(writer, "Remove", "bool", $"{prefix}Remove", interopType, $", {k} key"); + EmitUnsafeAccessor(writer, "TryGetValue", "bool", $"{prefix}TryGetValue", interopType, $", {k} key, out {v} value"); + EmitUnsafeAccessor(writer, "Add", "void", $"{prefix}Add", interopType, $", {kv} item"); + EmitUnsafeAccessor(writer, "Clear", "void", $"{prefix}Clear", interopType, ""); + EmitUnsafeAccessor(writer, "Contains", "bool", $"{prefix}Contains", interopType, $", {kv} item"); + EmitUnsafeAccessor(writer, "CopyTo", "void", $"{prefix}CopyTo", interopType, $", WindowsRuntimeObjectReference enumObjRef, {kv}[] array, int arrayIndex"); + EmitUnsafeAccessor(writer, "Remove", "bool", $"{prefix}Remove", interopType, $", {kv} item"); + + // Public member emission order matches the WinRT IMap vtable order, NOT alphabetical. + // GetEnumerator is NOT emitted here -- it's handled separately by IIterable's own + // EmitGenericEnumerable invocation. + writer.WriteLine($$""" + public ICollection<{{k}}> Keys => {{prefix}}Keys(null, {{objRefName}}); + public ICollection<{{v}}> Values => {{prefix}}Values(null, {{objRefName}}); + public int Count => {{prefix}}Count(null, {{objRefName}}); + public bool IsReadOnly => false; + public {{v}} this[{{k}} key] + { + get => {{prefix}}Item(null, {{objRefName}}, key); + set => {{prefix}}Item(null, {{objRefName}}, key, value); + } + public void Add({{k}} key, {{v}} value) => {{prefix}}Add(null, {{objRefName}}, key, value); + public bool ContainsKey({{k}} key) => {{prefix}}ContainsKey(null, {{objRefName}}, key); + public bool Remove({{k}} key) => {{prefix}}Remove(null, {{objRefName}}, key); + public bool TryGetValue({{k}} key, out {{v}} value) => {{prefix}}TryGetValue(null, {{objRefName}}, key, out value); + public void Add({{kv}} item) => {{prefix}}Add(null, {{objRefName}}, item); + public void Clear() => {{prefix}}Clear(null, {{objRefName}}); + public bool Contains({{kv}} item) => {{prefix}}Contains(null, {{objRefName}}, item); + public void CopyTo({{kv}}[] array, int arrayIndex) => {{prefix}}CopyTo(null, {{objRefName}}, {{enumerableObjRefName}}, array, arrayIndex); + bool ICollection<{{kv}}>.Remove({{kv}} item) => {{prefix}}Remove(null, {{objRefName}}, item); + """, isMultiline: true); + } + + private static void EmitReadOnlyDictionary(IndentedTextWriter writer, ProjectionEmitContext context, List args, List argSigs, string objRefName) + { + if (args.Count != 2) + { + return; + } + + string k = WriteTypeNameToString(context, args[0], TypedefNameType.Projected, true); + string v = WriteTypeNameToString(context, args[1], TypedefNameType.Projected, true); + string keyId = EncodeArgIdentifier(context, args[0]); + string valId = EncodeArgIdentifier(context, args[1]); + string keyInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string valInteropArg = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[1], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<" + keyInteropArg + "|" + valInteropArg + ">Methods, WinRT.Interop"; + string prefix = "IReadOnlyDictionaryMethods_" + keyId + "_" + valId + "_"; + + writer.WriteLine(); + EmitUnsafeAccessor(writer, "Keys", $"ICollection<{k}>", $"{prefix}Keys", interopType, ""); + EmitUnsafeAccessor(writer, "Values", $"ICollection<{v}>", $"{prefix}Values", interopType, ""); + EmitUnsafeAccessor(writer, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(writer, "Item", v, $"{prefix}Item", interopType, $", {k} key"); + EmitUnsafeAccessor(writer, "ContainsKey", "bool", $"{prefix}ContainsKey", interopType, $", {k} key"); + EmitUnsafeAccessor(writer, "TryGetValue", "bool", $"{prefix}TryGetValue", interopType, $", {k} key, out {v} value"); + + // GetEnumerator is NOT emitted here -- it's handled separately by IIterable's + // EmitGenericEnumerable invocation. + writer.WriteLine(); + writer.WriteLine($"public {v} this[{k} key] => {prefix}Item(null, {objRefName}, key);"); + writer.WriteLine($"public IEnumerable<{k}> Keys => {prefix}Keys(null, {objRefName});"); + writer.WriteLine($"public IEnumerable<{v}> Values => {prefix}Values(null, {objRefName});"); + writer.WriteLine($"public int Count => {prefix}Count(null, {objRefName});"); + writer.WriteLine($"public bool ContainsKey({k} key) => {prefix}ContainsKey(null, {objRefName}, key);"); + writer.WriteLine($"public bool TryGetValue({k} key, out {v} value) => {prefix}TryGetValue(null, {objRefName}, key, out value);"); + } + + private static void EmitReadOnlyList(IndentedTextWriter writer, ProjectionEmitContext context, List args, List argSigs, string objRefName) + { + if (args.Count != 1) + { + return; + } + + string t = WriteTypeNameToString(context, args[0], TypedefNameType.Projected, true); + string elementId = EncodeArgIdentifier(context, args[0]); + string interopTypeArgs = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IReadOnlyList'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IReadOnlyListMethods_" + elementId + "_"; + + writer.WriteLine(); + EmitUnsafeAccessor(writer, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(writer, "Item", t, $"{prefix}Item", interopType, ", int index"); + + // GetEnumerator is NOT emitted here -- it's handled separately by IIterable's + // EmitGenericEnumerable invocation. + writer.WriteLine(); + writer.WriteLine($$""" + [global::System.Runtime.CompilerServices.IndexerName("ReadOnlyListItem")] + public {{t}} this[int index] => {{prefix}}Item(null, {{objRefName}}, index); + public int Count => {{prefix}}Count(null, {{objRefName}}); + """, isMultiline: true); + } + + /// + /// Writes a projected type name to a scratch buffer and returns the string. + /// + private static string WriteTypeNameToString(ProjectionEmitContext context, TypeSemantics arg, TypedefNameType nameType, bool forceQualified) + { + string result = TypedefNameWriter.WriteTypeName(context, arg, nameType, forceQualified); + return result; + } + + /// + /// Encodes a type semantics as a C# identifier-safe name. Uses the projected type name + /// WITHOUT forcing namespace qualification, then strips 'global::' and replaces '.' with '_'. + /// + private static string EncodeArgIdentifier(ProjectionEmitContext context, TypeSemantics arg) + { + string projected = WriteTypeNameToString(context, arg, TypedefNameType.Projected, false); + return IidExpressionGenerator.EscapeTypeNameForIdentifier(projected, stripGlobal: true); + } + + private static void EmitList(IndentedTextWriter writer, ProjectionEmitContext context, List args, List argSigs, string objRefName) + { + if (args.Count != 1) + { + return; + } + + string t = WriteTypeNameToString(context, args[0], TypedefNameType.Projected, true); + string elementId = EncodeArgIdentifier(context, args[0]); + string interopTypeArgs = InteropTypeNameWriter.EncodeInteropTypeName(argSigs[0], TypedefNameType.Projected); + string interopType = "ABI.System.Collections.Generic.<#corlib>IList'1<" + interopTypeArgs + ">Methods, WinRT.Interop"; + string prefix = "IListMethods_" + elementId + "_"; + + writer.WriteLine(); + EmitUnsafeAccessor(writer, "Count", "int", $"{prefix}Count", interopType, ""); + EmitUnsafeAccessor(writer, "Item", t, $"{prefix}Item", interopType, ", int index"); + EmitUnsafeAccessor(writer, "Item", "void", $"{prefix}Item", interopType, $", int index, {t} value"); + EmitUnsafeAccessor(writer, "IndexOf", "int", $"{prefix}IndexOf", interopType, $", {t} item"); + EmitUnsafeAccessor(writer, "Insert", "void", $"{prefix}Insert", interopType, $", int index, {t} item"); + EmitUnsafeAccessor(writer, "RemoveAt", "void", $"{prefix}RemoveAt", interopType, ", int index"); + EmitUnsafeAccessor(writer, "Add", "void", $"{prefix}Add", interopType, $", {t} item"); + EmitUnsafeAccessor(writer, "Clear", "void", $"{prefix}Clear", interopType, ""); + EmitUnsafeAccessor(writer, "Contains", "bool", $"{prefix}Contains", interopType, $", {t} item"); + EmitUnsafeAccessor(writer, "CopyTo", "void", $"{prefix}CopyTo", interopType, $", {t}[] array, int arrayIndex"); + EmitUnsafeAccessor(writer, "Remove", "bool", $"{prefix}Remove", interopType, $", {t} item"); + + // Public member emission order matches the WinRT IVector vtable order mapped to IList, + // NOT alphabetical. GetEnumerator is NOT emitted here -- it's handled separately by IIterable's + // own EmitGenericEnumerable invocation. + writer.WriteLine($$""" + public int Count => {{prefix}}Count(null, {{objRefName}}); + public bool IsReadOnly => false; + + [global::System.Runtime.CompilerServices.IndexerName("ListItem")] + public {{t}} this[int index] + { + get => {{prefix}}Item(null, {{objRefName}}, index); + set => {{prefix}}Item(null, {{objRefName}}, index, value); + } + public int IndexOf({{t}} item) => {{prefix}}IndexOf(null, {{objRefName}}, item); + public void Insert(int index, {{t}} item) => {{prefix}}Insert(null, {{objRefName}}, index, item); + public void RemoveAt(int index) => {{prefix}}RemoveAt(null, {{objRefName}}, index); + public void Add({{t}} item) => {{prefix}}Add(null, {{objRefName}}, item); + public void Clear() => {{prefix}}Clear(null, {{objRefName}}); + public bool Contains({{t}} item) => {{prefix}}Contains(null, {{objRefName}}, item); + public void CopyTo({{t}}[] array, int arrayIndex) => {{prefix}}CopyTo(null, {{objRefName}}, array, arrayIndex); + public bool Remove({{t}} item) => {{prefix}}Remove(null, {{objRefName}}, item); + """, isMultiline: true); + } + + /// + /// Emits a single [UnsafeAccessor] static extern declaration that targets a method on a + /// WinRT.Interop helper type. The function signature is built from the supplied parts. + /// + private static void EmitUnsafeAccessor(IndentedTextWriter writer, string accessName, string returnType, string functionName, string interopType, string extraParams) + { + writer.WriteLine($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "{{accessName}}")] + static extern {{returnType}} {{functionName}}([UnsafeAccessorType("{{interopType}}")] object _, WindowsRuntimeObjectReference objRef{{extraParams}}); + """, isMultiline: true); + writer.WriteLine(); + } + + private static void EmitNonGenericList(IndentedTextWriter writer, string objRefName) + { + writer.WriteLine(); + writer.WriteLine($$""" + [global::System.Runtime.CompilerServices.IndexerName("NonGenericListItem")] + public object this[int index] + { + get => global::ABI.System.Collections.IListMethods.Item({{objRefName}}, index); + set => global::ABI.System.Collections.IListMethods.Item({{objRefName}}, index, value); + } + public int Count => global::ABI.System.Collections.IListMethods.Count({{objRefName}}); + public bool IsReadOnly => false; + public bool IsFixedSize => false; + public bool IsSynchronized => false; + public object SyncRoot => this; + public int Add(object value) => global::ABI.System.Collections.IListMethods.Add({{objRefName}}, value); + public void Clear() => global::ABI.System.Collections.IListMethods.Clear({{objRefName}}); + public bool Contains(object value) => global::ABI.System.Collections.IListMethods.Contains({{objRefName}}, value); + public int IndexOf(object value) => global::ABI.System.Collections.IListMethods.IndexOf({{objRefName}}, value); + public void Insert(int index, object value) => global::ABI.System.Collections.IListMethods.Insert({{objRefName}}, index, value); + public void Remove(object value) => global::ABI.System.Collections.IListMethods.Remove({{objRefName}}, value); + public void RemoveAt(int index) => global::ABI.System.Collections.IListMethods.RemoveAt({{objRefName}}, index); + public void CopyTo(Array array, int index) => global::ABI.System.Collections.IListMethods.CopyTo({{objRefName}}, array, index); + """, isMultiline: true); + // GetEnumerator is NOT emitted here -- it's handled separately by IBindableIterable's + // EmitNonGenericEnumerable invocation. + } +} diff --git a/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs b/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs new file mode 100644 index 0000000000..5dedfc9a58 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Builders; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Helper writers for assembly attributes, metadata attributes, file headers, and +/// other infrastructure that runs at the top or bottom of every emitted projection +/// file. +/// +internal static class MetadataAttributeFactory +{ + /// + /// Writes #pragma warning disable IL2026. + /// + /// The writer to emit to. + public static void WritePragmaDisableIL2026(IndentedTextWriter writer) + { + writer.WriteLine("#pragma warning disable IL2026"); + } + + /// + /// Writes #pragma warning restore IL2026. + /// + /// The writer to emit to. + public static void WritePragmaRestoreIL2026(IndentedTextWriter writer) + { + writer.WriteLine(); + writer.WriteLine("#pragma warning restore IL2026"); + } + + /// + /// Returns the version string embedded in the banner comment of generated files. + /// MSBuild and defaults to 0.0.0-private.0). + /// We read the writer assembly's + /// (set via $(InformationalVersion)) and strip any SourceLink commit-sha suffix + /// after a '+' so the banner is reproducible across rebuilds of the same source. + /// + internal static string GetVersionString() + { + Assembly asm = typeof(ProjectionFileBuilder).Assembly; + AssemblyInformationalVersionAttribute? attr = + (AssemblyInformationalVersionAttribute?)Attribute.GetCustomAttribute( + asm, typeof(AssemblyInformationalVersionAttribute)); + string version = attr?.InformationalVersion ?? "0.0.0-private.0"; + int plus = version.IndexOf('+'); + return plus >= 0 ? version[..plus] : version; + } + + /// + /// Writes the standard auto-generated banner comment (no using imports, no pragmas). + /// Used for the leaner WinRT_Module.cs / GeneratedInterfaceIIDs.cs / + /// Resources/Base/*.cs output prelude. Distinct from + /// , which also emits the canonical + /// using imports + suppression pragmas. + /// + /// The writer to emit the banner to. + public static void WriteFileHeader(IndentedTextWriter writer) + { + writer.WriteLine($$""" + //------------------------------------------------------------------------------ + // + // This file was generated by cswinrt.exe version {{GetVersionString()}} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + """, isMultiline: true); + } + + /// + /// Convenience overload of that leases an + /// from , emits the + /// auto-generated banner into it, and returns the resulting string. + /// + /// The emitted auto-generated banner. + public static string GetFileHeader() + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteFileHeader(writer); + return writer.ToString(); + } + + /// + /// Writes a [WindowsRuntimeMetadata] attribute decorating with its source .winmd module name. + /// + /// The writer to emit to. + /// The type definition. + /// The metadata cache used to resolve the source module path. + public static void WriteWinRTMetadataAttribute(IndentedTextWriter writer, TypeDefinition type, MetadataCache cache) + { + string path = cache.GetSourcePath(type); + string stem = string.IsNullOrEmpty(path) ? string.Empty : Path.GetFileNameWithoutExtension(path); + writer.WriteLine($"[WindowsRuntimeMetadata(\"{stem}\")]"); + } + + /// + /// Writes a [WindowsRuntimeMetadataTypeName] attribute carrying the WinRT type name string. + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + public static void WriteWinRTMetadataTypeNameAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + writer.Write("[WindowsRuntimeMetadataTypeName(\""); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.NonProjected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine("\")]"); + } + + /// + /// Writes a [WindowsRuntimeMappedType] attribute pointing at the projected type. + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + public static void WriteWinRTMappedTypeAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + writer.Write("[WindowsRuntimeMappedType(typeof("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine("))]"); + } + + /// + /// Writes a [WindowsRuntimeClassName("Windows.Foundation.IReference`1<NS.Name>")] attribute for a value type. + /// + /// The writer to emit to. + /// The active emit context. + /// The value type definition. + public static void WriteValueTypeWinRTClassNameAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.ReferenceProjection) + { + return; + } + + (string ns, string name) = type.Names(); + writer.WriteLine($"[WindowsRuntimeClassName(\"Windows.Foundation.IReference`1<{ns}.{name}>\")]"); + } + + /// + /// Writes a [WindowsRuntimeReferenceType(typeof(NullableX))] attribute on a reference type. + /// + /// The writer to emit to. + /// The active emit context. + /// The reference type definition. + public static void WriteWinRTReferenceTypeAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.ReferenceProjection) + { + return; + } + + writer.Write("[WindowsRuntimeReferenceType(typeof("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, false); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine("?))]"); + } + + /// + /// Writes the [ABI.NS.NameComWrappersMarshaller] attribute. + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + public static void WriteComWrapperMarshallerAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (context.Settings.ReferenceProjection) + { + return; + } + + (string ns, string name) = type.Names(); + writer.WriteLine($"[ABI.{ns}.{IdentifierEscaping.StripBackticks(name)}ComWrappersMarshaller]"); + } + + /// + /// Writes the [assembly: TypeMap<WindowsRuntimeMetadataTypeMapGroup>] attribute + /// for an authored / projected type. + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + public static void WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // Skip exclusive interfaces and projection-internal interfaces. + if (TypeCategorization.GetCategory(type) == TypeCategory.Interface && + (TypeCategorization.IsExclusiveTo(type) || TypeCategorization.IsProjectionInternal(type))) + { + return; + } + + // Capture the projected type name as a string by writing into a scratch writer at indent 0. + string projectionName = TypedefNameWriter.WriteTypedefNameWithTypeParams(context, type, TypedefNameType.NonProjected, true); + + writer.WriteLine(); + writer.Write($$""" + [assembly: TypeMap( + value: "{{projectionName}}", + target: typeof( + """, isMultiline: true); + if (context.Settings.Component) + { + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, true); + TypedefNameWriter.WriteTypeParams(writer, type); + } + else + { + writer.Write(projectionName); + } + + writer.WriteLine($$""" + ), + trimTarget: typeof({{projectionName}}))] + """, isMultiline: true); + + if (context.Settings.Component) + { + writer.WriteLine(); + writer.Write($$""" + [assembly: TypeMapAssociation( + source: typeof({{projectionName}}), + proxy: typeof( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine("))]"); + writer.WriteLine(); + } + } + + /// + /// Writes the [assembly: TypeMap<WindowsRuntimeComWrappersTypeMapGroup>] attribute + /// for the type's ComWrappers marshalling registration. + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + /// When , wraps the projected type in Windows.Foundation.IReference`1<...>. + public static void WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool isValueType) + { + string projectionName = TypedefNameWriter.WriteTypedefNameWithTypeParams(context, type, TypedefNameType.NonProjected, true); + + writer.WriteLine(); + writer.Write(""" + [assembly: TypeMap( + value: " + """, isMultiline: true); + if (isValueType) + { + writer.Write($"Windows.Foundation.IReference`1<{projectionName}>"); + } + else + { + writer.Write(projectionName); + } + + writer.Write(""" + ", + target: typeof( + """, isMultiline: true); + if (context.Settings.Component) + { + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, true); + TypedefNameWriter.WriteTypeParams(writer, type); + } + else + { + writer.Write(projectionName); + } + + writer.WriteLine($$""" + ), + trimTarget: typeof({{projectionName}}))] + """, isMultiline: true); + + // For non-interface, non-struct authored types, emit proxy association. + TypeCategory cat = TypeCategorization.GetCategory(type); + + if (cat is not (TypeCategory.Interface or TypeCategory.Struct) && context.Settings.Component) + { + writer.WriteLine(); + writer.Write($$""" + [assembly: TypeMapAssociation( + source: typeof({{projectionName}}), + proxy: typeof( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine("))]"); + writer.WriteLine(); + } + } + + /// + /// Writes the [assembly: TypeMapAssociation<DynamicInterfaceCastableImplementationTypeMapGroup>] + /// attribute for an interface's dynamic-interface-castable implementation registration. + /// + /// The writer to emit to. + /// The active emit context. + /// The interface type definition. + public static void WriteWinRTIdicTypeMapGroupAssemblyAttribute(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // Generic interfaces are handled elsewhere. + if (type.GenericParameters.Count != 0) + { + return; + } + + // Skip exclusive interfaces (unless idic_exclusiveto), and projection-internal types. + if ((TypeCategorization.IsExclusiveTo(type) && !context.Settings.IdicExclusiveTo) || + TypeCategorization.IsProjectionInternal(type)) + { + return; + } + + writer.WriteLine(); + writer.Write(""" + [assembly: TypeMapAssociation( + source: typeof( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(""" + ), + proxy: typeof( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.WriteLine("))]"); + writer.WriteLine(); + } + + /// + /// Adds an entry to the default-interface map for a class type. + /// + public static void AddDefaultInterfaceEntry(ProjectionEmitContext context, TypeDefinition type, System.Collections.Concurrent.ConcurrentDictionary entries) + { + if (context.Settings.ReferenceProjection) + { + return; + } + + ITypeDefOrRef? defaultIface = type.GetDefaultInterface(); + + if (defaultIface is null) + { + return; + } + + (string typeNs, string typeName) = type.Names(); + string className = $"global::{typeNs}.{IdentifierEscaping.StripBackticks(typeName)}"; + + // Resolve TypeReference -> TypeDefinition so WriteTypeName goes through the Definition + // branch which knows about authored-type CCW namespacing (ABI.Impl. prefix). + ITypeDefOrRef capturedIface = defaultIface; + + if (capturedIface is not TypeDefinition && capturedIface is not TypeSpecification && context.Cache is not null) + { + TypeDefinition? resolved = capturedIface.TryResolve(context.Cache.RuntimeContext); + + if (resolved is not null) + { + capturedIface = resolved; + } + } + + // Build the interface display name via TypeSemantics so generic instantiations + // (e.g. IDictionary), TypeRefs and TypeDefs are all handled correctly. + string interfaceName = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.GetFromTypeDefOrRef(capturedIface), TypedefNameType.CCW, true); + + _ = entries.TryAdd(className, interfaceName); + } + + /// + /// Adds entries for [ExclusiveTo] interfaces of the class type. + /// + public static void AddExclusiveToInterfaceEntries(ProjectionEmitContext context, TypeDefinition type, System.Collections.Concurrent.ConcurrentBag> entries) + { + if (!context.Settings.Component || context.Settings.ReferenceProjection) + { + return; + } + + (string typeNs, string typeName) = type.Names(); + string className = $"global::{typeNs}.{IdentifierEscaping.StripBackticks(typeName)}"; + + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + // Resolve the interface to a TypeDefinition for the [ExclusiveTo] check. + TypeDefinition? ifaceDef = impl.Interface as TypeDefinition; + + if (ifaceDef is null && context.Cache is not null) + { + ifaceDef = impl.Interface.TryResolve(context.Cache.RuntimeContext); + } + + if (ifaceDef is null && impl.Interface is TypeSpecification spec + && spec.Signature is GenericInstanceTypeSignature gi) + { + ifaceDef = gi.GenericType as TypeDefinition; + + if (ifaceDef is null && context.Cache is not null) + { + ifaceDef = gi.GenericType.TryResolve(context.Cache.RuntimeContext); + } + } + + if (ifaceDef is null) + { + continue; + } + + if (TypeCategorization.IsExclusiveTo(ifaceDef)) + { + // Resolve TypeReference -> TypeDefinition so WriteTypeName goes through the + // Definition branch which knows about authored-type CCW namespacing. + ITypeDefOrRef capturedIface = impl.Interface; + + if (capturedIface is not TypeDefinition && capturedIface is not TypeSpecification && context.Cache is not null) + { + TypeDefinition? resolved = capturedIface.TryResolve(context.Cache.RuntimeContext); + + if (resolved is not null) + { + capturedIface = resolved; + } + } + + string interfaceName = TypedefNameWriter.WriteTypeName(context, TypeSemanticsFactory.GetFromTypeDefOrRef(capturedIface), TypedefNameType.CCW, true); + entries.Add(new KeyValuePair(className, interfaceName)); + } + } + } + + /// + /// Writes the generated WindowsRuntimeDefaultInterfaces.cs file. + /// + public static void WriteDefaultInterfacesClass(Settings settings, IReadOnlyList> sortedEntries) + { + if (sortedEntries.Count == 0) + { + return; + } + + using IndentedTextWriterOwner wOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter w = wOwner.Writer; + WriteFileHeader(w); + w.WriteLine("using System;"); + w.WriteLine("using WindowsRuntime;"); + w.WriteLine(); + w.WriteLine("#pragma warning disable CSWINRT3001"); + w.WriteLine(); + w.WriteLine("namespace ABI"); + using (w.WriteBlock()) + { + foreach (KeyValuePair kv in sortedEntries) + { + w.WriteLine($"[WindowsRuntimeDefaultInterface(typeof({kv.Key}), typeof({kv.Value}))]"); + } + + w.WriteLine("internal static class WindowsRuntimeDefaultInterfaces;"); + } + + w.FlushToFile(Path.Combine(settings.OutputFolder, "WindowsRuntimeDefaultInterfaces.cs")); + } + + /// + /// Writes the generated WindowsRuntimeExclusiveToInterfaces.cs file. + /// + public static void WriteExclusiveToInterfacesClass(Settings settings, IReadOnlyList> sortedEntries) + { + if (sortedEntries.Count == 0) + { + return; + } + + using IndentedTextWriterOwner wOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter w = wOwner.Writer; + WriteFileHeader(w); + w.WriteLine("using System;"); + w.WriteLine("using WindowsRuntime;"); + w.WriteLine(); + w.WriteLine("#pragma warning disable CSWINRT3001"); + w.WriteLine(); + w.WriteLine("namespace ABI"); + using (w.WriteBlock()) + { + foreach (KeyValuePair kv in sortedEntries) + { + w.WriteLine($"[WindowsRuntimeExclusiveToInterface(typeof({kv.Key}), typeof({kv.Value}))]"); + } + + w.WriteLine("internal static class WindowsRuntimeExclusiveToInterfaces;"); + } + + w.FlushToFile(Path.Combine(settings.OutputFolder, "WindowsRuntimeExclusiveToInterfaces.cs")); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/MethodFactory.cs b/src/WinRT.Projection.Writer/Factories/MethodFactory.cs new file mode 100644 index 0000000000..216a213a12 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/MethodFactory.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Builders; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Helpers for method/parameter/return type emission. +/// +internal static class MethodFactory +{ + /// + /// Writes the projected C# type for the given . + /// + /// The writer to emit to. + /// The active emit context. + /// The signature to project. + /// When , projects SZ-arrays as (parameter convention) instead of T[]. + public static void WriteProjectedSignature(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature typeSig, bool isParameter) + { + if (typeSig is SzArrayTypeSignature sz) + { + // SZ arrays project as ReadOnlySpan (matches the property setter parameter + // convention; pass_array semantics). + if (isParameter) + { + writer.Write("ReadOnlySpan<"); + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(sz.BaseType)); + writer.Write(">"); + } + else + { + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(sz.BaseType)); + writer.Write("[]"); + } + + return; + } + + if (typeSig is ByReferenceTypeSignature br) + { + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(br.BaseType)); + return; + } + + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(typeSig)); + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the projected signature into it, and returns the resulting string. + /// + /// The active emit context. + /// The signature to project. + /// When , projects SZ-arrays as (parameter convention) instead of T[]. + /// The projected signature. + public static string WriteProjectedSignature(ProjectionEmitContext context, TypeSignature typeSig, bool isParameter) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteProjectedSignature(writer, context, typeSig, isParameter); + return writer.ToString(); + } + + /// + /// Writes a parameter's projected type, applying the -specific transformations. + /// + /// The writer to emit to. + /// The active emit context. + /// The parameter info. + public static void WriteProjectionParameterType(IndentedTextWriter writer, ProjectionEmitContext context, ParameterInfo p) + { + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + switch (cat) + { + case ParameterCategory.Out: + writer.Write("out "); + WriteProjectedSignature(writer, context, p.Type, true); + break; + case ParameterCategory.Ref: + writer.Write("in "); + WriteProjectedSignature(writer, context, p.Type, true); + break; + case ParameterCategory.PassArray: + writer.Write("ReadOnlySpan<"); + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); + writer.Write(">"); + break; + case ParameterCategory.FillArray: + writer.Write("Span<"); + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(((SzArrayTypeSignature)p.Type).BaseType)); + writer.Write(">"); + break; + case ParameterCategory.ReceiveArray: + writer.Write("out "); + SzArrayTypeSignature? sz = p.Type as SzArrayTypeSignature + ?? (p.Type is ByReferenceTypeSignature br ? br.BaseType as SzArrayTypeSignature : null); + + if (sz is not null) + { + TypedefNameWriter.WriteProjectionType(writer, context, TypeSemanticsFactory.Get(sz.BaseType)); + writer.Write("[]"); + } + else + { + WriteProjectedSignature(writer, context, p.Type, true); + } + break; + default: + WriteProjectedSignature(writer, context, p.Type, true); + break; + } + } + + /// + /// Writes the parameter name (escaped if it would clash with a C# keyword). + /// + /// The writer to emit to. + /// The parameter info. + public static void WriteParameterName(IndentedTextWriter writer, ParameterInfo p) + { + string name = p.Parameter.Name ?? "param"; + + if (CSharpKeywords.IsKeyword(name)) + { + writer.Write("@"); + } + + writer.Write(name); + } + + /// + /// Writes the parameter's projected type + name (e.g. int @value). + /// + /// The writer to emit to. + /// The active emit context. + /// The parameter info. + public static void WriteProjectionParameter(IndentedTextWriter writer, ProjectionEmitContext context, ParameterInfo p) + { + WriteProjectionParameterType(writer, context, p); + writer.Write(" "); + WriteParameterName(writer, p); + } + + /// + /// Writes the projected return type of (or "void"). + /// + /// The writer to emit to. + /// The active emit context. + /// The method signature. + public static void WriteProjectionReturnType(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig) + { + TypeSignature? rt = sig.ReturnType; + + if (rt is null) + { + writer.Write("void"); + return; + } + + WriteProjectedSignature(writer, context, rt, false); + } + + /// + /// Writes a comma-separated parameter list. + /// + /// The writer to emit to. + /// The active emit context. + /// The method signature whose parameters to enumerate. + public static void WriteParameterList(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig) + { + for (int i = 0; i < sig.Parameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + WriteProjectionParameter(writer, context, sig.Parameters[i]); + } + } + + /// + /// Returns the C# literal text for a constant field's value (or empty when no constant). + /// + /// The field definition. + /// The formatted constant value, or an empty string. + public static string FormatField(FieldDefinition field) + { + if (field.Constant is null) + { + return string.Empty; + } + + return ProjectionFileBuilder.FormatConstant(field.Constant); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/RefModeStubFactory.cs b/src/WinRT.Projection.Writer/Factories/RefModeStubFactory.cs new file mode 100644 index 0000000000..518e09a17f --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/RefModeStubFactory.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Reference-projection stub emission helpers. In reference projection mode, all method/property/ +/// event bodies (and certain other constructs like static factory objref getters, activation +/// factory objref getters, and the synthetic private ctor for classes without explicit +/// constructors) collapse to throw null. +/// +internal static class RefModeStubFactory +{ + /// + /// Emits the body of an _objRef_* property getter in reference projection mode. + /// + /// The writer to emit to. + public static void EmitRefModeObjRefGetterBody(IndentedTextWriter writer) + { + writer.WriteLine(); + writer.WriteLine(""" + { + get + { + throw null; + } + } + """, isMultiline: true); + } + + /// + /// Emits the synthetic private TypeName() { throw null; } ctor used in reference + /// projection mode to suppress the C# compiler's implicit public default constructor when + /// no explicit ctors are emitted by WriteAttributedTypes. + /// + /// The writer to emit to. + /// The type name to emit the synthetic constructor for. + public static void EmitSyntheticPrivateCtor(IndentedTextWriter writer, string typeName) + { + writer.WriteLine(); + writer.WriteLine($"private {typeName}() {{ throw null; }}"); + } + + /// + /// Emits the body of a delegate factory Invoke method in reference projection mode. + /// + /// The writer to emit to. + public static void EmitRefModeInvokeBody(IndentedTextWriter writer) + { + writer.WriteLine(""" + throw null; + } + } + """, isMultiline: true); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ReferenceImplFactory.cs b/src/WinRT.Projection.Writer/Factories/ReferenceImplFactory.cs new file mode 100644 index 0000000000..99b97a9e80 --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/ReferenceImplFactory.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the IReference<T> implementation class for a struct/enum/delegate type +/// (the boxed-value adapter that exposes the value through the WinRT IReference COM interface). +/// +internal static class ReferenceImplFactory +{ + /// + /// Writes the IReference impl class for a struct/enum/delegate type. + /// + /// The writer to emit to. + /// The active emit context. + /// The type definition. + public static void WriteReferenceImpl(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + string visibility = context.Settings.Component ? "public" : "file"; + bool blittable = AbiTypeHelpers.IsTypeBlittable(context.Cache, type); + + writer.WriteLine(); + writer.WriteLine($$""" + {{visibility}} static unsafe class {{nameStripped}}ReferenceImpl + { + [FixedAddressValueType] + private static readonly ReferenceVftbl Vftbl; + + static {{nameStripped}}ReferenceImpl() + { + *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable; + Vftbl.get_Value = &get_Value; + } + + public static nint Vtable + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)Unsafe.AsPointer(in Vftbl); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + """, isMultiline: true); + bool isBlittableStructType = blittable && TypeCategorization.GetCategory(type) == TypeCategory.Struct; + bool isNonBlittableStructType = !blittable && TypeCategorization.GetCategory(type) == TypeCategory.Struct; + + if ((blittable && TypeCategorization.GetCategory(type) != TypeCategory.Struct) + || isBlittableStructType) + { + // For blittable types and blittable structs: direct memcpy via C# struct assignment. + // Even bool/char fields work because their managed layout matches the WinRT ABI. + writer.Write(""" + public static int get_Value(void* thisPtr, void* result) + { + if (result is null) + { + return unchecked((int)0x80004003); + } + + try + { + var value = ( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.Write(""" + )(ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr)); + *( + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.WriteLine(""" + *)result = value; + return 0; + } + catch (Exception e) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e); + } + } + """, isMultiline: true); + } + else if (isNonBlittableStructType) + { + // Non-blittable struct: marshal via Marshaller.ConvertToUnmanaged then write the + // (ABI) struct value into the result pointer. + string projectedName = MethodFactory.WriteProjectedSignature(context, type.ToTypeSignature(), false); + string abiName = AbiTypeHelpers.GetAbiStructTypeName(writer, context, type.ToTypeSignature()); + writer.WriteLine($$""" + public static int get_Value(void* thisPtr, void* result) + { + if (result is null) + { + return unchecked((int)0x80004003); + } + + try + { + {{projectedName}} unboxedValue = ({{projectedName}})ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + {{abiName}} value = {{nameStripped}}Marshaller.ConvertToUnmanaged(unboxedValue); + *({{abiName}}*)result = value; + return 0; + } + catch (Exception e) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e); + } + } + """, isMultiline: true); + } + else if (TypeCategorization.GetCategory(type) is TypeCategory.Class or TypeCategory.Delegate) + { + // Non-blittable runtime class / delegate: marshal via Marshaller and detach. + string projectedName = MethodFactory.WriteProjectedSignature(context, type.ToTypeSignature(), false); + writer.WriteLine($$""" + public static int get_Value(void* thisPtr, void* result) + { + if (result is null) + { + return unchecked((int)0x80004003); + } + + try + { + {{projectedName}} unboxedValue = ({{projectedName}})ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + void* value = {{nameStripped}}Marshaller.ConvertToUnmanaged(unboxedValue).DetachThisPtrUnsafe(); + *(void**)result = value; + return 0; + } + catch (Exception e) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(e); + } + } + """, isMultiline: true); + } + else + { + // Defensive: should be unreachable. WriteReferenceImpl is only called for enum/struct/delegate + // types (WriteAbiEnum / WriteAbiStruct / WriteAbiDelegate dispatchers). + throw WellKnownProjectionWriterExceptions.UnreachableEmissionState( + $"WriteReferenceImpl: unsupported type category {TypeCategorization.GetCategory(type)} " + + $"for type '{type.FullName}'. Expected enum/struct/delegate."); + } + + // IID property: 'public static ref readonly Guid IID' pointing at the reference type's IID. + writer.WriteLine(); + writer.Write(""" + public static ref readonly Guid IID + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref global::ABI.InterfaceIIDs. + """, isMultiline: true); + IidExpressionGenerator.WriteIidReferenceGuidPropertyName(writer, context, type); + writer.WriteLine(""" + ; + } + } + """, isMultiline: true); + writer.WriteLine(); + } +} diff --git a/src/WinRT.Projection.Writer/Factories/StructEnumMarshallerFactory.cs b/src/WinRT.Projection.Writer/Factories/StructEnumMarshallerFactory.cs new file mode 100644 index 0000000000..8a76a6670d --- /dev/null +++ b/src/WinRT.Projection.Writer/Factories/StructEnumMarshallerFactory.cs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Factories; + +/// +/// Emits the static marshaller class for a complex struct or enum type (managed-to-ABI/ABI-to-managed conversion, blittable detection, etc.). +/// +internal static class StructEnumMarshallerFactory +{ + /// + /// Writes a marshaller class for a struct or enum. + /// + internal static void WriteStructEnumMarshallerClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = type.Name?.Value ?? string.Empty; + string nameStripped = IdentifierEscaping.StripBackticks(name); + TypeCategory cat = TypeCategorization.GetCategory(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). + TypeDefOrRefSignature sig = type.ToTypeSignature(false) is TypeDefOrRefSignature td2 ? td2 : null!; + bool almostBlittable = cat == TypeCategory.Struct && (sig is null || context.AbiTypeShapeResolver.IsBlittableStruct(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 . + bool hasReferenceFields = false; + + if (isComplexStruct) + { + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + TypeSignature ft = field.Signature.FieldType; + + if (AbiTypeHelpers.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, string typeNm) = type.Names(); + bool isMappedStruct = isComplexStruct && MappedTypes.Get(typeNs, typeNm) is not null; + + if (isMappedStruct) + { + isComplexStruct = false; + } + + writer.WriteLine($$""" + public static unsafe class {{nameStripped}}Marshaller + { + """, isMultiline: true); + + if (isComplexStruct) + { + // ConvertToUnmanaged: build ABI struct from projected struct via per-field marshalling. + writer.Write(" public static "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, false); + writer.Write(" ConvertToUnmanaged("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.WriteLine(""" + value) + { + return new() { + """, isMultiline: true); + bool first = true; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + string fname = field.Name?.Value ?? ""; + TypeSignature ft = field.Signature.FieldType; + + if (!first) + { + writer.WriteLine(","); + } + + first = false; + writer.Write($" {fname} = "); + + if (ft.IsString()) + { + writer.Write($"HStringMarshaller.ConvertToUnmanaged(value.{fname})"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(ft)) + { + writer.Write($"{AbiTypeHelpers.GetMappedMarshallerName(ft)}.ConvertToUnmanaged(value.{fname})"); + } + else if (ft.IsHResultException()) + { + // 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). + writer.Write($"global::ABI.System.ExceptionMarshaller.ConvertToUnmanaged(value.{fname})"); + } + else if (ft is TypeDefOrRefSignature ftd + && AbiTypeHelpers.TryResolveStructTypeDef(context.Cache, ftd) is TypeDefinition fieldStructTd + && TypeCategorization.GetCategory(fieldStructTd) == TypeCategory.Struct + && !AbiTypeHelpers.IsTypeBlittable(context.Cache, fieldStructTd)) + { + // Nested non-blittable struct: marshal via its Marshaller. + writer.Write($"{IdentifierEscaping.StripBackticks(fieldStructTd.Name?.Value ?? string.Empty)}Marshaller.ConvertToUnmanaged(value.{fname})"); + } + else if (AbiTypeHelpers.TryGetNullablePrimitiveMarshallerName(ft, out string? nullableMarshaller)) + { + writer.Write($"{nullableMarshaller!}.BoxToUnmanaged(value.{fname}).DetachThisPtrUnsafe()"); + } + else + { + writer.Write($"value.{fname}"); + } + } + writer.WriteLine(); + writer.Write(""" + }; + } + public static + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.Write(" ConvertToManaged("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, false); + // - 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 = context.Settings.Component; + writer.Write(""" + value) + { + return new + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.WriteLine(useObjectInitializer ? "(){" : "("); + first = true; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + string fname = field.Name?.Value ?? ""; + TypeSignature ft = field.Signature.FieldType; + + if (!first) + { + writer.WriteLine(","); + } + + first = false; + writer.Write(" "); + + if (useObjectInitializer) + { + writer.Write($"{fname} = "); + } + + if (ft.IsString()) + { + writer.Write($"HStringMarshaller.ConvertToManaged(value.{fname})"); + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(ft)) + { + writer.Write($"{AbiTypeHelpers.GetMappedMarshallerName(ft)}.ConvertToManaged(value.{fname})"); + } + else if (ft.IsHResultException()) + { + // 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). + writer.Write($"global::ABI.System.ExceptionMarshaller.ConvertToManaged(value.{fname})"); + } + else if (ft is TypeDefOrRefSignature ftd2 + && AbiTypeHelpers.TryResolveStructTypeDef(context.Cache, ftd2) is TypeDefinition fieldStructTd2 + && TypeCategorization.GetCategory(fieldStructTd2) == TypeCategory.Struct + && !AbiTypeHelpers.IsTypeBlittable(context.Cache, fieldStructTd2)) + { + // Nested non-blittable struct: convert via its Marshaller. + writer.Write($"{IdentifierEscaping.StripBackticks(fieldStructTd2.Name?.Value ?? string.Empty)}Marshaller.ConvertToManaged(value.{fname})"); + } + else if (AbiTypeHelpers.TryGetNullablePrimitiveMarshallerName(ft, out string? nullableMarshaller)) + { + writer.Write($"{nullableMarshaller!}.UnboxToManaged(value.{fname})"); + } + else + { + writer.Write($"value.{fname}"); + } + } + writer.WriteLine(); + writer.WriteLine(useObjectInitializer ? " };" : " );"); + writer.WriteLine(" }"); + writer.Write(" public static void Dispose("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, false); + writer.WriteLine(""" + value) + { + """, isMultiline: true); + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + string fname = field.Name?.Value ?? ""; + TypeSignature ft = field.Signature.FieldType; + + if (ft.IsString()) + { + writer.WriteLine($" HStringMarshaller.Free(value.{fname});"); + } + else if (ft.IsHResultException()) + { + // HResult/Exception field has no per-value resources to release + // (the ABI representation is just an int HRESULT). Skip Dispose entirely. + continue; + } + else if (context.AbiTypeShapeResolver.IsMappedAbiValueType(ft)) + { + // Mapped value types (DateTime/TimeSpan) have no per-value resources to + // release — the ABI representation is just an int64 + continue; + } + else if (ft is TypeDefOrRefSignature ftd3 + && AbiTypeHelpers.TryResolveStructTypeDef(context.Cache, ftd3) is TypeDefinition fieldStructTd3 + && TypeCategorization.GetCategory(fieldStructTd3) == TypeCategory.Struct + && !AbiTypeHelpers.IsTypeBlittable(context.Cache, fieldStructTd3)) + { + // Nested non-blittable struct: dispose via its Marshaller. + string nestedNs = fieldStructTd3.Namespace?.Value ?? string.Empty; + string nestedNm = IdentifierEscaping.StripBackticks(fieldStructTd3.Name?.Value ?? string.Empty); + writer.WriteLine($" global::ABI.{nestedNs}.{nestedNm}Marshaller.Dispose(value.{fname});"); + } + else if (AbiTypeHelpers.TryGetNullablePrimitiveMarshallerName(ft, out _)) + { + writer.WriteLine($" WindowsRuntimeUnknownMarshaller.Free(value.{fname});"); + } + } + writer.WriteLine(" }"); + } + + // 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. + writer.Write(" public static WindowsRuntimeObjectReferenceValue BoxToUnmanaged("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + + if (isEnum || almostBlittable || isComplexStruct) + { + writer.Write($$""" + ? value) + { + return WindowsRuntimeValueTypeMarshaller.BoxToUnmanaged(value, CreateComInterfaceFlags.{{(hasReferenceFields ? "TrackerSupport" : "None")}}, in + """, isMultiline: true); + ObjRefNameGenerator.WriteIidReferenceExpression(writer, type); + writer.WriteLine(""" + ); + } + """, isMultiline: true); + } + 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). + writer.Write(""" + ? value) + { + return WindowsRuntimeValueTypeMarshaller.BoxToUnmanaged(value, CreateComInterfaceFlags.None, in + """, isMultiline: true); + ObjRefNameGenerator.WriteIidReferenceExpression(writer, type); + writer.WriteLine(""" + ); + } + """, isMultiline: true); + } + + // UnboxToManaged: simple for almost-blittable; for complex, unbox to ABI struct then ConvertToManaged. + writer.Write(" public static "); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + + if (isEnum || almostBlittable) + { + writer.Write(""" + ? UnboxToManaged(void* value) + { + return WindowsRuntimeValueTypeMarshaller.UnboxToManaged< + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.WriteLine(""" + >(value); + } + """, isMultiline: true); + } + else if (isComplexStruct) + { + writer.WriteLine(""" + ? UnboxToManaged(void* value) + { + + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, false); + writer.Write("? abi = WindowsRuntimeValueTypeMarshaller.UnboxToManaged<"); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, false); + writer.WriteLine(""" + >(value); + return abi.HasValue ? ConvertToManaged(abi.GetValueOrDefault()) : null; + } + """, isMultiline: true); + } + 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). + writer.Write(""" + ? UnboxToManaged(void* value) + { + return WindowsRuntimeValueTypeMarshaller.UnboxToManaged< + """, isMultiline: true); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.WriteLine(""" + >(value); + } + """, isMultiline: true); + } + + writer.WriteLine("}"); + writer.WriteLine(); + + // 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 = ObjRefNameGenerator.WriteIidReferenceExpression(type); + + // InterfaceEntriesImpl + writer.WriteLine($$""" + file static class {{nameStripped}}InterfaceEntriesImpl + { + [FixedAddressValueType] + public static readonly ReferenceInterfaceEntries Entries; + + static {{nameStripped}}InterfaceEntriesImpl() + { + Entries.IReferenceValue.IID = {{iidRefExpr}}; + Entries.IReferenceValue.Vtable = {{nameStripped}}ReferenceImpl.Vtable; + Entries.IPropertyValue.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IPropertyValue; + Entries.IPropertyValue.Vtable = global::WindowsRuntime.InteropServices.IPropertyValueImpl.OtherTypeVtable; + Entries.IStringable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable; + Entries.IStringable.Vtable = global::WindowsRuntime.InteropServices.IStringableImpl.Vtable; + Entries.IWeakReferenceSource.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IWeakReferenceSource; + Entries.IWeakReferenceSource.Vtable = global::WindowsRuntime.InteropServices.IWeakReferenceSourceImpl.Vtable; + Entries.IMarshal.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IMarshal; + Entries.IMarshal.Vtable = global::WindowsRuntime.InteropServices.IMarshalImpl.Vtable; + Entries.IAgileObject.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IAgileObject; + Entries.IAgileObject.Vtable = global::WindowsRuntime.InteropServices.IAgileObjectImpl.Vtable; + Entries.IInspectable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IInspectable; + Entries.IInspectable.Vtable = global::WindowsRuntime.InteropServices.IInspectableImpl.Vtable; + Entries.IUnknown.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IUnknown; + Entries.IUnknown.Vtable = global::WindowsRuntime.InteropServices.IUnknownImpl.Vtable; + } + } + """, isMultiline: true); + writer.WriteLine(); + // 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 (context.Settings.Component && cat == TypeCategory.Struct) + { + return; + } + + // ComWrappersMarshallerAttribute (full body) + writer.WriteLine($$""" + internal sealed unsafe class {{nameStripped}}ComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute + { + public override void* GetOrCreateComInterfaceForObject(object value) + { + return WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.{{(hasReferenceFields ? "TrackerSupport" : "None")}}); + } + + public override ComInterfaceEntry* ComputeVtables(out int count) + { + count = sizeof(ReferenceInterfaceEntries) / sizeof(ComInterfaceEntry); + return (ComInterfaceEntry*)Unsafe.AsPointer(in {{nameStripped}}InterfaceEntriesImpl.Entries); + } + + public override object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) + { + wrapperFlags = CreatedWrapperFlags.NonWrapping; + """, isMultiline: true); + if (isComplexStruct) + { + writer.Write($" return {nameStripped}Marshaller.ConvertToManaged(WindowsRuntimeValueTypeMarshaller.UnboxToManagedUnsafe<"); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.ABI, true); + writer.WriteLine($">(value, in {iidRefExpr}));"); + } + else + { + writer.Write(" return WindowsRuntimeValueTypeMarshaller.UnboxToManagedUnsafe<"); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.Projected, true); + writer.WriteLine($">(value, in {iidRefExpr});"); + } + + writer.WriteLine(""" + } + } + """, isMultiline: true); + } + else + { + // Fallback: keep the placeholder class so consumer attribute references resolve. + writer.WriteLine($$""" + internal sealed class {{nameStripped}}ComWrappersMarshallerAttribute : global::System.Attribute + { + } + """, isMultiline: true); + } + } +} diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs b/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs new file mode 100644 index 0000000000..36330867ee --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Resolvers; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +/// Per-emission context bundling all state shared by the projection writers when emitting a +/// single projection (settings, metadata cache, the active namespace, scoped emission-mode flags). +/// +/// The active projection settings. +/// The metadata cache for the current generation. +/// The namespace currently being emitted (or when not in a per-namespace pass). +internal sealed class ProjectionEmitContext(Settings settings, MetadataCache cache, string currentNamespace) +{ + /// + /// Gets the active projection settings. + /// + public Settings Settings { get; } = settings; + + /// + /// Gets the metadata cache for the current generation. + /// + public MetadataCache Cache { get; } = cache; + + /// + /// Gets the namespace currently being emitted, or when not in a per-namespace pass. + /// + public string CurrentNamespace { get; } = currentNamespace; + + /// + /// Gets a value indicating whether the writer is currently emitting inside an + /// ABI.<Ns> namespace block. Set by + /// and reset by + /// . Used by + /// to force the global::<Ns>. prefix on + /// projected type references inside the ABI block (the ABI section can't see the projected + /// namespace via using directives, so unqualified names would fail to resolve). + /// + public bool InAbiNamespace { get; set; } + + /// + /// Gets a value indicating whether the writer is currently emitting inside an + /// ABI.Impl.<Ns> namespace block (component-mode authored projections). + /// + public bool InAbiImplNamespace { get; set; } + + /// + /// Gets the resolver used to classify type signatures by their ABI marshalling shape. + /// + public AbiTypeShapeResolver AbiTypeShapeResolver { get; } = new AbiTypeShapeResolver(cache); + + /// + /// Gets a value indicating whether platform-attribute computation should suppress platforms + /// that are less than or equal to . Used to apply class-scope platform + /// suppression so member-level [SupportedOSPlatform] attributes don't repeat + /// information already on the enclosing type. Set via . + /// + public bool CheckPlatform { get; private set; } + + /// + /// Gets the active platform string for the platform-attribute suppression mode. Set initially + /// by ; subsequently seeded by the + /// platform-attribute algorithm on the first non-empty observation within the scope via + /// . + /// + public string Platform { get; private set; } = string.Empty; + + /// + /// Seeds with the first non-empty platform observed by the platform- + /// attribute algorithm within an active suppression scope. No-op outside a scope. + /// + /// The platform string observed at the current emission site. + public void SeedPlatform(string platform) + { + if (CheckPlatform && Platform.Length == 0) + { + Platform = platform; + } + } + + /// + /// Enters platform-attribute suppression mode for the given . + /// Returns an token that resets and + /// on dispose. Use as + /// using (context.EnterPlatformSuppressionScope(platform)) { ... }. + /// + /// The platform string for which member-level attributes are suppressed. + /// The scope token. + public PlatformSuppressionScope EnterPlatformSuppressionScope(string platform) + { + bool prevCheck = CheckPlatform; + string prevPlatform = Platform; + CheckPlatform = true; + Platform = platform; + return new PlatformSuppressionScope(this, prevCheck, prevPlatform); + } + + /// + /// Scope token for . + /// + public ref struct PlatformSuppressionScope : IDisposable + { + private ProjectionEmitContext? _context; + private readonly bool _prevCheck; + private readonly string _prevPlatform; + + internal PlatformSuppressionScope(ProjectionEmitContext context, bool prevCheck, string prevPlatform) + { + _context = context; + _prevCheck = prevCheck; + _prevPlatform = prevPlatform; + } + + /// + /// Restores the prior platform-suppression state. + /// + public void Dispose() + { + if (_context is { } context) + { + context.CheckPlatform = _prevCheck; + context.Platform = _prevPlatform; + _context = null; + } + } + } +} diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs new file mode 100644 index 0000000000..5c66a0f547 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.IO; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +internal sealed partial class ProjectionGenerator +{ + /// + /// Discovers all component-mode activatable runtime classes (those carrying + /// [ActivatableAttribute] or [StaticAttribute]) across the cached + /// namespaces and groups them by source .winmd module name. + /// + /// + /// A tuple of: + /// + /// ComponentActivatable -- the flat set of all activatable classes + /// ByModule -- the same set keyed by source module name (used to emit per-module activation-factory entry points in ) + /// + /// + private (HashSet ComponentActivatable, Dictionary> ByModule) DiscoverComponentActivatableTypes() + { + HashSet componentActivatable = []; + Dictionary> componentByModule = []; + + if (!_settings.Component) + { + return (componentActivatable, componentByModule); + } + + foreach ((_, NamespaceMembers members) in _cache.Namespaces) + { + foreach (TypeDefinition type in members.Classes) + { + if (!_settings.Filter.Includes(type)) + { + continue; + } + + if (type.HasAttribute(WindowsFoundationMetadata, ActivatableAttribute) || + type.HasAttribute(WindowsFoundationMetadata, StaticAttribute)) + { + _ = componentActivatable.Add(type); + string moduleName = Path.GetFileNameWithoutExtension(_cache.GetSourcePath(type)); + + if (!componentByModule.TryGetValue(moduleName, out HashSet? set)) + { + set = []; + componentByModule[moduleName] = set; + } + + _ = set.Add(type); + } + } + } + + return (componentActivatable, componentByModule); + } + + /// + /// Writes the WinRT_Module.cs file containing the per-module activation factory + /// entry points. Component mode only. + /// + /// The activatable classes grouped by source module name (from ). + internal void WriteComponentModuleFile(Dictionary> componentByModule) + { + // WinRT_Module.cs (and similar support files like GeneratedInterfaceIIDs.cs and the + // base resources under Resources/Base/) require only the auto-generated banner without + // the standard usings/pragmas prelude that per-namespace projection files emit, so this + // path goes through MetadataAttributeFactory.WriteFileHeader rather than the + // IndentedTextWriter extension that includes the full prelude. + using IndentedTextWriterOwner wmOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter wm = wmOwner.Writer; + MetadataAttributeFactory.WriteFileHeader(wm); + ComponentFactory.WriteModuleActivationFactory(wm, componentByModule); + wm.FlushToFile(Path.Combine(_settings.OutputFolder, "WinRT_Module.cs")); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs new file mode 100644 index 0000000000..2b5b0e10ec --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +internal sealed partial class ProjectionGenerator +{ + /// + /// Writes the GeneratedInterfaceIIDs.cs file containing the IID GUID property + /// definitions for every projected interface, delegate, enum, struct, and runtime class. + /// + /// + /// Skipped entirely in reference-projection mode (no IIDs are needed in the public API surface). + /// + internal void WriteGeneratedInterfaceIidsFile() + { + if (_settings.ReferenceProjection) + { + return; + } + + // 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 = []; + 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, string clsNm) = type.Names(); + MappedType? clsMapped = MappedTypes.Get(clsNs, clsNm); + + if (clsMapped is { EmitAbi: false }) + { + continue; + } + + AddFactoryInterfacesForClass(type, factoryInterfacesGlobal); + } + } + + bool iidWritten = false; + HashSet interfacesFromClassesEmitted = []; + ProjectionEmitContext guidContext = new(_settings, _cache, "ABI"); + using IndentedTextWriterOwner guidIndentedOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter guidIndented = guidIndentedOwner.Writer; + IidExpressionGenerator.WriteInterfaceIidsBegin(guidIndented); + guidIndented.IncreaseIndent(); + // Iterate namespaces in sorted order. 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, StringComparer.Ordinal)) + { + _token.ThrowIfCancellationRequested(); + 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, string nm2) = type.Names(); + MappedType? m = MappedTypes.Get(ns2, nm2); + + if (m is { EmitAbi: false }) + { + continue; + } + + iidWritten = true; + TypeCategory cat = TypeCategorization.GetCategory(type); + switch (cat) + { + case TypeCategory.Delegate: + IidExpressionGenerator.WriteIidGuidPropertyFromSignature(guidIndented, guidContext, type); + IidExpressionGenerator.WriteIidGuidPropertyFromType(guidIndented, guidContext, type); + break; + case TypeCategory.Enum: + IidExpressionGenerator.WriteIidGuidPropertyFromSignature(guidIndented, guidContext, type); + break; + case TypeCategory.Interface: + IidExpressionGenerator.WriteIidGuidPropertyFromType(guidIndented, guidContext, type); + break; + case TypeCategory.Struct: + IidExpressionGenerator.WriteIidGuidPropertyFromSignature(guidIndented, guidContext, type); + break; + case TypeCategory.Class: + IidExpressionGenerator.WriteIidGuidPropertyForClassInterfaces(guidIndented, guidContext, type, interfacesFromClassesEmitted); + break; + } + } + } + + IidExpressionGenerator.WriteInterfaceIidsEnd(guidIndented); + + if (iidWritten) + { + guidIndented.FlushToFile(Path.Combine(_settings.OutputFolder, "GeneratedInterfaceIIDs.cs")); + } + } +} diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs new file mode 100644 index 0000000000..51f2601dbc --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Builders; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +internal sealed partial class ProjectionGenerator +{ + /// + /// Processes a single namespace and writes its projection file. Returns whether a file was written. + /// + internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGeneratorRunState state) + { + ConcurrentDictionary defaultInterfaceEntries = state.DefaultInterfaceEntries; + ConcurrentBag> exclusiveToInterfaceEntries = state.ExclusiveToInterfaceEntries; + ConcurrentDictionary authoredTypeNameToMetadataMap = state.AuthoredTypeNameToMetadataMap; + HashSet componentActivatable = state.ComponentActivatable; + ProjectionEmitContext context = new(_settings, _cache, ns); + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + + writer.WriteFileHeader(context); + + bool written = false; + + // Phase 1: TypeMapGroup assembly attributes + _token.ThrowIfCancellationRequested(); + if (!_settings.ReferenceProjection) + { + writer.WriteLine(); + MetadataAttributeFactory.WritePragmaDisableIL2026(writer); + foreach (TypeDefinition type in members.Types) + { + if (!_settings.Filter.Includes(type)) + { + continue; + } + + if (TypeCategorization.IsGeneric(type)) + { + continue; + } + + (string ns2, string nm2) = type.Names(); + MappedType? m = MappedTypes.Get(ns2, nm2); + + if (m is { EmitAbi: false }) + { + continue; + } + + TypeCategory cat = TypeCategorization.GetCategory(type); + switch (cat) + { + case TypeCategory.Class: + if (!TypeCategorization.IsStatic(type) && !TypeCategorization.IsAttributeType(type)) + { + if (_settings.Component) + { + MetadataAttributeFactory.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(writer, context, type); + } + else + { + MetadataAttributeFactory.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(writer, context, type, false); + } + } + + break; + case TypeCategory.Delegate: + MetadataAttributeFactory.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(writer, context, type, true); + MetadataAttributeFactory.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(writer, context, type); + break; + case TypeCategory.Enum: + MetadataAttributeFactory.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(writer, context, type, true); + MetadataAttributeFactory.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(writer, context, type); + break; + case TypeCategory.Interface: + MetadataAttributeFactory.WriteWinRTIdicTypeMapGroupAssemblyAttribute(writer, context, type); + MetadataAttributeFactory.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(writer, context, type); + break; + case TypeCategory.Struct: + if (!TypeCategorization.IsApiContractType(type)) + { + MetadataAttributeFactory.WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(writer, context, type, true); + MetadataAttributeFactory.WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(writer, context, type); + } + + break; + } + } + + MetadataAttributeFactory.WritePragmaRestoreIL2026(writer); + } + + // Phase 2: Projected types + _token.ThrowIfCancellationRequested(); + writer.WriteBeginProjectedNamespace(context); + + foreach (TypeDefinition type in members.Types) + { + if (!_settings.Filter.Includes(type)) + { + continue; + } + + (string ns2, string nm2) = type.Names(); + // Skip generic types and mapped types + 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); + ProjectionFileBuilder.WriteType(writer, context, type, category); + + if (category == TypeCategory.Class && !TypeCategorization.IsAttributeType(type)) + { + MetadataAttributeFactory.AddDefaultInterfaceEntry(context, type, defaultInterfaceEntries); + MetadataAttributeFactory.AddExclusiveToInterfaceEntries(context, type, exclusiveToInterfaceEntries); + ComponentFactory.AddMetadataTypeEntry(context, type, authoredTypeNameToMetadataMap); + + if (_settings.Component && componentActivatable.Contains(type)) + { + ComponentFactory.WriteFactoryClass(writer, context, type); + } + } + else if (category is TypeCategory.Delegate or TypeCategory.Enum or TypeCategory.Interface) + { + ComponentFactory.AddMetadataTypeEntry(context, type, authoredTypeNameToMetadataMap); + } + else if (category == TypeCategory.Struct && !TypeCategorization.IsApiContractType(type)) + { + ComponentFactory.AddMetadataTypeEntry(context, type, authoredTypeNameToMetadataMap); + } + + written = true; + } + + writer.WriteEndProjectedNamespace(context); + + if (!written) + { + return false; + } + + // Phase 3: ABI types (when not reference projection) + _token.ThrowIfCancellationRequested(); + 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. + HashSet factoryInterfacesInThisNs = []; + HashSet factoryInterfacesAllNs = []; + foreach (TypeDefinition type in members.Types) + { + if (!_settings.Filter.Includes(type)) + { + continue; + } + + if (TypeCategorization.GetCategory(type) != TypeCategory.Class) + { + continue; + } + + AddFactoryInterfacesForClass(type, factoryInterfacesAllNs); + } + foreach (TypeDefinition facType in factoryInterfacesAllNs) + { + // Only consider factory interfaces in the same namespace as we're processing. + string facNs = facType.Namespace?.Value ?? string.Empty; + + if (facNs == ns) + { + _ = factoryInterfacesInThisNs.Add(facType); + } + } + + writer.WriteBeginAbiNamespace(context); + 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, string nm2) = type.Names(); + MappedType? m = MappedTypes.Get(ns2, nm2); + + if (m is { EmitAbi: false }) + { + continue; + } + + if (TypeCategorization.IsApiContractType(type)) + { + continue; + } + + if (TypeCategorization.IsAttributeType(type)) + { + continue; + } + + TypeCategory category = TypeCategorization.GetCategory(type); + ProjectionFileBuilder.WriteAbiType(writer, context, type, category); + } + writer.WriteEndAbiNamespace(context); + } + + // Phase 4: Custom additions to namespaces + _token.ThrowIfCancellationRequested(); + if (Additions.ByNamespace.TryGetValue(ns, out string[]? resourceNames) && _settings.AdditionFilter.Includes(ns)) + { + foreach (string resName in resourceNames) + { + using Stream? stream = typeof(ProjectionWriter).Assembly.GetManifestResourceStream(resName); + + if (stream is null) + { + continue; + } + + using StreamReader reader = new(stream); + string content = reader.ReadToEnd(); + writer.Write(content); + } + } + + // Output to file + string filename = ns + ".cs"; + string fullPath = Path.Combine(_settings.OutputFolder, filename); + writer.FlushToFile(fullPath); + return true; + } +} diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs new file mode 100644 index 0000000000..3e69d3ef89 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Resources.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Reflection; +using WindowsRuntime.ProjectionWriter.Factories; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +internal sealed partial class ProjectionGenerator +{ + /// + /// The embedded-resource manifest segment that identifies a "base" resource (i.e. one that + /// gets emitted verbatim into every projection output folder). + /// + private const string ResourcesBaseSegment = ".Resources.Base."; + + /// + /// 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.ProjectionWriter.Resources.Base.ComInteropExtensions.cs' + if (!resName.Contains(ResourcesBaseSegment)) + { + continue; + } + + // Skip ComInteropExtensions if Windows is not included + string fileName = resName[(resName.IndexOf(ResourcesBaseSegment, StringComparison.Ordinal) + ResourcesBaseSegment.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; + } + + // Each base resource gets the standard auto-generated file header prepended. + string header = MetadataAttributeFactory.GetFileHeader(); + + string outPath = Path.Combine(_settings.OutputFolder, fileName); + File.WriteAllText(outPath, header + content); + } + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs new file mode 100644 index 0000000000..1fd63d2481 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Generation.WorkItems; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +/// Orchestrates the projection generation: discovers component-activatable types, walks each +/// namespace in the metadata cache and emits the per-namespace .cs files, then writes +/// the per-projection support files (default-interfaces map, exclusive-to map, base resources). +/// +/// +/// +/// Work is split into discrete units (one per namespace, plus +/// the global GeneratedInterfaceIIDs.cs file and -- in component mode -- the +/// WinRT_Module.cs activation-factory aggregator). Items are dispatched in parallel via +/// +/// with the configured ; cross-item shared state +/// lives in and uses +/// / / so the per-item bodies can run +/// without explicit locking. +/// +/// +/// Discovery (component activation lookups) and post-processing (default-interfaces table, +/// exclusive-to-interfaces table, base-resource emission) remain sequential -- the former +/// because it produces the read-only state every work item depends on, the latter because they +/// consume the accumulated post-loop state. +/// +/// +/// The active projection settings. +/// The metadata cache built from the input .winmd files. +/// The cancellation token observed across all phases. +internal sealed partial class ProjectionGenerator(Settings settings, MetadataCache cache, CancellationToken token) +{ + private readonly Settings _settings = settings; + private readonly MetadataCache _cache = cache; + private readonly CancellationToken _token = token; + + /// + /// Runs the projection-generation pipeline end-to-end. + /// + public void Run() + { + HashSet componentActivatable; + Dictionary> componentByModule; + + // Phase 1: discover the activatable runtime classes (component mode only). + try + { + (componentActivatable, componentByModule) = DiscoverComponentActivatableTypes(); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledProjectionWriterException("discovery", e); + } + + _token.ThrowIfCancellationRequested(); + + ProjectionGeneratorRunState state = new(componentActivatable, componentByModule); + + // Phase 3..6: parallel emission. All file writes happen below; wrap the whole emission + // pipeline in a single try/catch so any unexpected failure surfaces as an + // UnhandledProjectionWriterException rather than a raw stack trace. + try + { + if (_settings.Verbose) + { + Action log = _settings.Logger ?? Console.Out.WriteLine; + + foreach (string p in _settings.Input) + { + log($"input: {p}"); + } + + log($"output: {_settings.OutputFolder}"); + } + + ParallelOptions parallelOptions = new() + { + CancellationToken = _token, + MaxDegreeOfParallelism = _settings.MaxDegreesOfParallelism, + }; + + ParallelLoopResult result = Parallel.ForEach( + source: EnumerateWorkItems(state), + parallelOptions: parallelOptions, + body: static item => item.Execute()); + + // Defensive: should always be true (no break/stop in the body), but matches the + // interop generator's pattern. + if (!result.IsCompleted) + { + throw WellKnownProjectionWriterExceptions.WorkItemLoopDidNotComplete(); + } + + if (state.DefaultInterfaceEntries.Count > 0 && !_settings.ReferenceProjection) + { + List> sorted = [.. state.DefaultInterfaceEntries]; + sorted.Sort((a, b) => StringComparer.Ordinal.Compare(a.Key, b.Key)); + MetadataAttributeFactory.WriteDefaultInterfacesClass(_settings, sorted); + } + + if (!state.ExclusiveToInterfaceEntries.IsEmpty && _settings.Component && !_settings.ReferenceProjection) + { + List> sorted = [.. state.ExclusiveToInterfaceEntries]; + sorted.Sort((a, b) => StringComparer.Ordinal.Compare(a.Key, b.Key)); + MetadataAttributeFactory.WriteExclusiveToInterfacesClass(_settings, sorted); + } + + if (state.ProjectionFileWritten) + { + WriteBaseStrings(); + } + } + catch (AggregateException e) + { + Exception innerException = e.InnerExceptions.FirstOrDefault()!; + + // If the first inner exception is well known, just rethrow it. + // We're not concerned about always throwing the same one across + // re-runs with parallelism. It can be disabled for debugging. + throw innerException.IsWellKnown + ? innerException + : WellKnownProjectionWriterExceptions.WorkItemLoopError(innerException); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledProjectionWriterException("emit", e); + } + } + + /// + /// Enumerates the work items dispatched in parallel by : the global + /// GeneratedInterfaceIIDs.cs file (skipped in reference-projection mode), one item + /// per namespace in the metadata cache, and -- in component mode -- the + /// WinRT_Module.cs file. The component module item is yielded last so the other two + /// item kinds get a chance to start before the smaller (typically) component item picks up + /// any remaining slot. + /// + /// The shared run state passed to per-item factories. + /// The lazy work-item sequence. + private IEnumerable EnumerateWorkItems(ProjectionGeneratorRunState state) + { + if (!_settings.ReferenceProjection) + { + yield return new IidsWorkItem(this); + } + + foreach ((string ns, NamespaceMembers members) in _cache.Namespaces) + { + yield return new NamespaceWorkItem(this, ns, members, state); + } + + if (_settings.Component) + { + yield return new ComponentModuleWorkItem(this, state); + } + } + + /// + /// Adds the factory interfaces (Static / Activatable / Composable) referenced by + /// to . Used by both the global + /// GeneratedInterfaceIIDs.cs walk and the per-namespace ABI-emission walk. + /// + /// The runtime class definition to scan. + /// The set the factory interface definitions are added to. + private void AddFactoryInterfacesForClass(TypeDefinition classType, HashSet result) + { + foreach (KeyValuePair kv in AttributedTypes.Get(classType, _cache)) + { + TypeDefinition? facType = kv.Value.Type; + + if (facType is not null) + { + _ = result.Add(facType); + } + } + } +} diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs new file mode 100644 index 0000000000..915781e84a --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +/// Cross-work-item shared state for a single invocation. +/// Bundles the concurrent collections each per-namespace work item contributes to, the +/// pre-discovered component-activation lookup, and the "did any work item write a projection +/// file?" flag (tracked via so concurrent writes from any work item +/// produce a deterministic post-loop value). +/// +internal sealed class ProjectionGeneratorRunState +{ + /// + /// Gets the activatable runtime classes discovered during the (sequential) discovery phase. + /// Read-only after construction; safe for concurrent reads from any work item. + /// + public HashSet ComponentActivatable { get; } + + /// + /// Gets the activatable runtime classes grouped by source .winmd module name. + /// Read-only after construction; consumed by . + /// + public Dictionary> ComponentByModule { get; } + + /// + /// Gets the (projected-type-name -> default-interface-name) map populated by namespace + /// work items via . + /// + public ConcurrentDictionary DefaultInterfaceEntries { get; } = []; + + /// + /// Gets the (interface-name, parent-class-name) pairs populated by namespace work items via + /// . + /// + public ConcurrentBag> ExclusiveToInterfaceEntries { get; } = []; + + /// + /// Gets the (projected-type-name -> CCW-type-name) metadata map populated by namespace work + /// items via . + /// + public ConcurrentDictionary AuthoredTypeNameToMetadataMap { get; } = []; + + /// + /// Tracked via so any number of work items can mark "I wrote a + /// projection file" concurrently without a torn read. Use + /// to query (after the parallel loop completes) and + /// from inside a work item. + /// + private int _projectionFileWritten; + + /// + /// Initializes a new with the discovered + /// activatable-class lookups. + /// + /// The flat set of activatable runtime classes. + /// The activatable classes grouped by source module name. + public ProjectionGeneratorRunState( + HashSet componentActivatable, + Dictionary> componentByModule) + { + ComponentActivatable = componentActivatable; + ComponentByModule = componentByModule; + } + + /// + /// Gets whether any work item has marked a projection file as written. + /// Should only be queried after the parallel loop has completed. + /// + public bool ProjectionFileWritten => Volatile.Read(ref _projectionFileWritten) != 0; + + /// + /// Records that the calling work item wrote a projection file. + /// Safe to call concurrently from any number of work items. + /// + public void MarkProjectionFileWritten() + { + _ = Interlocked.Exchange(ref _projectionFileWritten, 1); + } +} diff --git a/src/WinRT.Projection.Writer/Generation/Settings.cs b/src/WinRT.Projection.Writer/Generation/Settings.cs new file mode 100644 index 0000000000..402d906bc5 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/Settings.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Helpers; + +namespace WindowsRuntime.ProjectionWriter.Generation; + +/// +/// Configuration bag for a projection-writer invocation: input metadata paths, output +/// folder, namespace include/exclude filters, and per-emission-mode flags (component, +/// reference projection, public enums, etc.). +/// +/// +/// Callers populate the mutable input/include/exclude sets and init properties up +/// front, then call exactly once before passing this instance +/// to . eagerly computes the +/// derived and so subsequent parallel +/// reads from work items have a stable, non-racy view. +/// +internal sealed class Settings +{ + /// + /// Indicates whether has been called. + /// + private volatile bool _isReadOnly; + + /// + /// Gets the set of input .winmd file paths to project. + /// + public HashSet Input { get; } = []; + + /// + /// Gets or sets the output folder where generated .cs files are written. + /// + public string OutputFolder { get; init; } = string.Empty; + + /// + /// Gets or sets a value indicating whether verbose progress is logged to the console. + /// + public bool Verbose { get; init; } + + /// + /// Optional callback invoked for each verbose progress message. When , + /// verbose messages are forwarded to . Has no effect unless + /// is also set. + /// + public Action? Logger { get; init; } + + /// + /// Maximum number of parallel work items dispatched when generating projections. + /// Defaults to -1 (let the runtime decide; typically ). + /// Set to 1 to force fully sequential execution. + /// + public int MaxDegreesOfParallelism { get; init; } = -1; + + /// + /// Gets the namespace prefixes to include in projection (when empty, all namespaces are included). + /// + public HashSet Include { get; } = []; + + /// + /// Gets the namespace prefixes to exclude from projection. + /// + public HashSet Exclude { get; } = []; + + /// + /// Gets the namespace prefixes whose namespace-additions resources should be excluded. + /// + public HashSet AdditionExclude { get; } = []; + + /// + /// Gets the compiled type-name filter built from and . + /// Only valid after has been called. + /// + /// + /// Thrown if accessed before has been called. + /// + public TypeFilter Filter + { + get => field ?? throw WellKnownProjectionWriterExceptions.SettingsNotReadOnly(); + private set; + } + + /// + /// Gets the compiled type-name filter built from and , used for namespace-additions resources only. + /// Only valid after has been called. + /// + /// + /// Thrown if accessed before has been called. + /// + public TypeFilter AdditionFilter + { + get => field ?? throw WellKnownProjectionWriterExceptions.SettingsNotReadOnly(); + private set; + } + + /// + /// Gets or sets a value indicating whether component-authoring mode is enabled. + /// + public bool Component { get; init; } + + /// + /// Gets or sets a value indicating whether projected types are emitted as internal rather than public. + /// + public bool Internal { get; init; } + + /// + /// Gets or sets a value indicating whether the projection is embedded into a consuming assembly (forces internal visibility). + /// + public bool Embedded { get; init; } + + /// + /// Gets or sets a value indicating whether projected enums are forced to public visibility (overrides ). + /// + public bool PublicEnums { get; init; } + + /// + /// Gets or sets a value indicating whether [ExclusiveTo] interfaces are emitted as public rather than internal. + /// + public bool PublicExclusiveTo { get; init; } + + /// + /// Gets or sets a value indicating whether the IDIC pattern is applied to [ExclusiveTo] interfaces. + /// + public bool IdicExclusiveTo { get; init; } + + /// + /// Gets or sets a value indicating whether reference-only projection mode is enabled (no implementation, no IID file). + /// + public bool ReferenceProjection { get; init; } + + /// + /// Finalizes the settings: eagerly builds the derived and + /// from the configured include/exclude sets, then marks + /// the instance as read-only. Must be called exactly once before passing the instance + /// to . + /// + /// + /// Thrown if was already called on this instance. + /// + public void MakeReadOnly() + { + if (_isReadOnly) + { + throw WellKnownProjectionWriterExceptions.SettingsAlreadyReadOnly(); + } + + Filter = new TypeFilter(Include, Exclude); + AdditionFilter = new TypeFilter(Include, AdditionExclude); + _isReadOnly = true; + } +} diff --git a/src/WinRT.Projection.Writer/Generation/WorkItems/ComponentModuleWorkItem.cs b/src/WinRT.Projection.Writer/Generation/WorkItems/ComponentModuleWorkItem.cs new file mode 100644 index 0000000000..0fef172ca1 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/WorkItems/ComponentModuleWorkItem.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.ProjectionWriter.Errors; + +namespace WindowsRuntime.ProjectionWriter.Generation.WorkItems; + +/// +/// Work item that emits the WinRT_Module.cs file containing the per-module activation- +/// factory entry points. Only enumerated when is set. +/// +/// The owning generator (provides access to settings + the component-module entry point). +/// The shared run state that this work item writes into (component module always counts as a written projection file). +internal sealed class ComponentModuleWorkItem( + ProjectionGenerator owner, + ProjectionGeneratorRunState state) : IProjectionWorkItem +{ + /// + public void Execute() + { + try + { + owner.WriteComponentModuleFile(state.ComponentByModule); + state.MarkProjectionFileWritten(); + } + catch (Exception e) + { + WellKnownProjectionWriterExceptions.ComponentModuleEmissionFailed(e).ThrowOrAttach(e); + } + } +} diff --git a/src/WinRT.Projection.Writer/Generation/WorkItems/IProjectionWorkItem.cs b/src/WinRT.Projection.Writer/Generation/WorkItems/IProjectionWorkItem.cs new file mode 100644 index 0000000000..04cb204ab2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/WorkItems/IProjectionWorkItem.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.Generation.WorkItems; + +/// +/// A single unit of work dispatched from to the parallel +/// orchestrator. Each implementation owns the inputs it needs to execute (closure-style) so the +/// orchestrator can simply iterate the work-item enumerable and call on +/// each item without knowing the per-item shape. +/// +internal interface IProjectionWorkItem +{ + /// + /// Executes the work item. Must be safe to invoke concurrently with sibling work items + /// produced by the same enumeration. + /// + void Execute(); +} diff --git a/src/WinRT.Projection.Writer/Generation/WorkItems/IidsWorkItem.cs b/src/WinRT.Projection.Writer/Generation/WorkItems/IidsWorkItem.cs new file mode 100644 index 0000000000..356db32455 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/WorkItems/IidsWorkItem.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.ProjectionWriter.Errors; + +namespace WindowsRuntime.ProjectionWriter.Generation.WorkItems; + +/// +/// Work item that emits the per-projection GeneratedInterfaceIIDs.cs file (the global +/// IID GUID property table for every projected interface, delegate, enum, struct, and runtime +/// class). Decoupled from per-namespace work items because it produces a single distinct output +/// file that does not contend with any other work item. +/// +/// The owning generator (provides access to settings, cache, and the IID-emission entry point). +internal sealed class IidsWorkItem(ProjectionGenerator owner) : IProjectionWorkItem +{ + /// + public void Execute() + { + try + { + owner.WriteGeneratedInterfaceIidsFile(); + } + catch (Exception e) + { + WellKnownProjectionWriterExceptions.GeneratedInterfaceIidsEmissionFailed(e).ThrowOrAttach(e); + } + } +} diff --git a/src/WinRT.Projection.Writer/Generation/WorkItems/NamespaceWorkItem.cs b/src/WinRT.Projection.Writer/Generation/WorkItems/NamespaceWorkItem.cs new file mode 100644 index 0000000000..0ac3a84d42 --- /dev/null +++ b/src/WinRT.Projection.Writer/Generation/WorkItems/NamespaceWorkItem.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Metadata; + +namespace WindowsRuntime.ProjectionWriter.Generation.WorkItems; + +/// +/// Work item that emits the projection .cs file for a single namespace. Each work item +/// owns its target namespace + its bag and contributes to the +/// shared through its concurrent collections. +/// +/// The owning generator (provides access to settings, cache, and the per-namespace emission entry point). +/// The namespace being processed. +/// The types in the target namespace. +/// The shared run state that this work item writes into. +internal sealed class NamespaceWorkItem( + ProjectionGenerator owner, + string ns, + NamespaceMembers members, + ProjectionGeneratorRunState state) : IProjectionWorkItem +{ + /// + public void Execute() + { + try + { + if (owner.ProcessNamespace(ns, members, state)) + { + state.MarkProjectionFileWritten(); + } + } + catch (Exception e) + { + WellKnownProjectionWriterExceptions.NamespaceEmissionFailed(ns, e).ThrowOrAttach(e); + } + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.AbiTypeNames.cs b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.AbiTypeNames.cs new file mode 100644 index 0000000000..05d08f91ba --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.AbiTypeNames.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +internal static partial class AbiTypeHelpers +{ + /// + /// Returns the ABI type name for a blittable struct (the projected type name). + /// + internal static string GetBlittableStructAbiType(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature sig) + { + // Mapped value types (DateTime/TimeSpan) use the ABI type, not the projected type. + if (IsMappedAbiValueType(sig)) + { + return GetMappedAbiTypeName(sig); + } + + string result = MethodFactory.WriteProjectedSignature(context, sig, false); + return result; + } + + /// Returns the ABI struct type name for a complex struct (e.g. global::ABI.Windows.Web.Http.HttpProgress). + /// When the writer is currently in the matching ABI namespace, returns just the + /// short type name (e.g. HttpProgress) to mirror the original code which uses the + /// unqualified name in same-namespace contexts. + internal static string GetAbiStructTypeName(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature sig) + { + if (sig is TypeDefOrRefSignature td) + { + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + // If this struct is mapped, use the mapped namespace+name (e.g. + // 'Windows.UI.Xaml.Interop.TypeName' is mapped to 'System.Type', so the ABI struct + // is 'global::ABI.System.Type', not 'global::ABI.Windows.UI.Xaml.Interop.TypeName'). + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + return GlobalAbiPrefix + ns + "." + IdentifierEscaping.StripBackticks(name); + } + + return "global::ABI.Object"; + } + + /// + /// Returns the C# primitive keyword (e.g. "bool", "int") for an ABI corlib element type, or when is not a primitive. + /// + internal static string GetAbiPrimitiveType(MetadataCache cache, TypeSignature sig) + { + if (sig is CorLibTypeSignature corlib) + { + return corlib.ElementType switch + { + ElementType.Boolean => "bool", + ElementType.Char => "char", + _ => GetAbiFundamentalTypeFromCorLib(corlib.ElementType), + }; + } + + // Enum: use the projected enum type as the ABI signature + if (sig is TypeDefOrRefSignature td) + { + TypeDefinition? def = td.Type as TypeDefinition; + + if (def is null && td.Type is TypeReference tr) + { + (string ns, string name) = tr.Names(); + def = cache.Find(ns + "." + name); + } + + if (def is not null && TypeCategorization.GetCategory(def) == TypeCategory.Enum) + { + return cache is null ? "int" : GetProjectedEnumName(def); + } + } + + return "int"; + } + + private static string GetProjectedEnumName(TypeDefinition def) + { + (string ns, string name) = def.Names(); + // Apply mapped-type translation so consumers see the projected (.NET) enum name + // (e.g. Windows.UI.Xaml.Interop.NotifyCollectionChangedAction → + // System.Collections.Specialized.NotifyCollectionChangedAction). Same + // remapping that WriteTypedefName performs. + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + return string.IsNullOrEmpty(ns) ? GlobalPrefix + name : GlobalPrefix + ns + "." + name; + } + + private static string GetAbiFundamentalTypeFromCorLib(ElementType et) + { + return et switch + { + ElementType.I1 => "sbyte", + ElementType.U1 => "byte", + ElementType.I2 => "short", + ElementType.U2 => "ushort", + ElementType.I4 => "int", + ElementType.U4 => "uint", + ElementType.I8 => "long", + ElementType.U8 => "ulong", + ElementType.R4 => "float", + ElementType.R8 => "double", + _ => "int", + }; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.Blittability.cs b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.Blittability.cs new file mode 100644 index 0000000000..636f9a47b9 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.Blittability.cs @@ -0,0 +1,426 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Metadata; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +internal static partial class AbiTypeHelpers +{ + /// + /// Returns whether the given type can be passed across the ABI boundary without per-field marshalling (struct layout matches the ABI representation). + /// + public static bool IsTypeBlittable(MetadataCache cache, TypeDefinition type) + { + TypeCategory cat = TypeCategorization.GetCategory(type); + + if (cat == TypeCategory.Enum) + { + return true; + } + + if (cat != TypeCategory.Struct) + { + return false; + } + + // 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, string name) = type.Names(); + + 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(cache, field.Signature.FieldType)) + { + return false; + } + } + return true; + } + + /// + /// Returns whether , treated as a struct-field type, is blittable + /// at the WinRT ABI. Used by to walk struct fields. + /// + /// The metadata cache used to resolve cross-module references. + /// The field signature to test. + /// if the field's storage layout matches its ABI layout. + internal static bool IsFieldTypeBlittable(MetadataCache cache, TypeSignature sig) + { + if (sig is CorLibTypeSignature corlib) + { + // ALL fundamentals (including Boolean, Char) are considered blittable here; + // only String is non-blittable. Object is not a fundamental — it's handled below. + return corlib.ElementType switch + { + ElementType.String => false, + ElementType.Object => false, + _ => true + }; + } + + // For TypeRef/TypeDef, resolve and check blittability. + if (sig is 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 . + // Same applies to System.IntPtr / UIntPtr (used in some struct layouts). + if (fNs == "System" && (fName is "Guid" or "IntPtr" || fName == "UIntPtr")) + { + return true; + } + + // Mapped struct types: blittable iff the mapping does NOT require marshalling + MappedType? mapped = MappedTypes.Get(fNs, fName); + + if (mapped is { RequiresMarshaling: true }) + { + return false; + } + + if (todr.Type is TypeDefinition td) + { + return IsTypeBlittable(cache, td); + } + + // Cross-module: try metadata cache. + if (todr.Type is TypeReference tr) + { + (string ns, string name) = tr.Names(); + TypeDefinition? resolved = cache.Find(ns + "." + name); + + if (resolved is not null) + { + return IsTypeBlittable(cache, 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. + /// + internal static TypeDefinition? TryResolveStructTypeDef(MetadataCache cache, TypeDefOrRefSignature tdr) + { + if (tdr.Type is TypeDefinition td) + { + return td; + } + + if (tdr.Type is TypeReference tr) + { + (string ns, string name) = tr.Names(); + return cache.Find(ns + "." + name); + } + + return null; + } + + /// + /// True if the type signature represents an enum (resolves cross-module typerefs). + /// + internal static bool IsEnumType(MetadataCache cache, TypeSignature sig) + { + if (sig is not TypeDefOrRefSignature td) + { + return false; + } + + if (td.Type is TypeDefinition def) + { + return TypeCategorization.GetCategory(def) == TypeCategory.Enum; + } + + if (td.Type is TypeReference tr) + { + (string ns, string name) = tr.Names(); + TypeDefinition? resolved = cache.Find(ns + "." + name); + return resolved is not null && TypeCategorization.GetCategory(resolved) == TypeCategory.Enum; + } + + return false; + } + + /// + /// True if the type signature represents a WinRT runtime class, interface, or delegate (reference type marshallable via *Marshaller). + /// + internal static bool IsRuntimeClassOrInterface(MetadataCache cache, TypeSignature sig) + { + if (sig is TypeDefOrRefSignature td) + { + // Same-module: use the resolved category directly. + if (td.Type is TypeDefinition def) + { + TypeCategory cat = TypeCategorization.GetCategory(def); + return cat is TypeCategory.Class or TypeCategory.Interface or TypeCategory.Delegate; + } + + // Cross-module typeref: try to resolve via the metadata cache to check category. + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + + if (ns == "System") + { + return name switch + { + "Uri" or "Type" or "IDisposable" or "Exception" => true, + _ => false, + }; + } + + if (cache is not null) + { + TypeDefinition? resolved = cache.Find(ns + "." + name); + + if (resolved is not null) + { + TypeCategory cat = TypeCategorization.GetCategory(resolved); + return cat is TypeCategory.Class or TypeCategory.Interface or TypeCategory.Delegate; + } + } + + // Unresolved cross-assembly TypeRef (e.g. a referenced winmd we don't have loaded). + // Fall back to the signature's encoding: WinRT metadata distinguishes value types + // (encoded as ValueType) from reference types (encoded as Class). If the signature + // has IsValueType == false, then it MUST be one of class/interface/delegate (since + // primitives/enums/strings/object are encoded with their own element type). This + // mirrors how the original code's abi_marshaler abstraction handles unknown types — it + // dispatches based on the metadata semantics, not on resolution. + return !td.IsValueType; + } + + return false; + } + + /// True if the type is a blittable primitive (or enum) directly representable + /// at the ABI: bool/byte/sbyte/short/ushort/int/uint/long/ulong/float/double/char and enums. + internal static bool IsBlittablePrimitive(MetadataCache cache, TypeSignature sig) + { + if (sig is CorLibTypeSignature corlib) + { + return corlib.ElementType is + ElementType.Boolean or + ElementType.I1 or + ElementType.U1 or + ElementType.I2 or + ElementType.U2 or + ElementType.I4 or + ElementType.U4 or + ElementType.I8 or + ElementType.U8 or + ElementType.R4 or + ElementType.R8 or + ElementType.Char; + } + + // Enum (TypeDefOrRef-based value type with non-Object base) - same module or cross-module + if (sig is TypeDefOrRefSignature td) + { + if (td.Type is TypeDefinition def && TypeCategorization.GetCategory(def) == TypeCategory.Enum) + { + return true; + } + + // Cross-module enum: try to resolve via the metadata cache. + if (td.Type is TypeReference tr) + { + (string ns, string name) = tr.Names(); + TypeDefinition? resolved = cache.Find(ns + "." + name); + + if (resolved is not null && TypeCategorization.GetCategory(resolved) == TypeCategory.Enum) + { + return true; + } + } + } + + return false; + } + + /// True for any struct type that can be passed directly across the WinRT ABI + /// (no per-field marshalling required). This includes blittable structs and "almost-blittable" + /// structs that have only primitive fields like bool/char (whose C# layout matches the WinRT ABI). + /// Excludes structs with reference type fields (string/object/runtime classes/etc.). + /// True for structs that have at least one reference type field (string, generic + /// instance Nullable<T>, etc.). These need per-field marshalling via the *Marshaller class + /// (ConvertToUnmanaged/ConvertToManaged/Dispose). + internal static bool IsComplexStruct(MetadataCache cache, TypeSignature sig) + { + if (sig is not TypeDefOrRefSignature td) + { + return false; + } + + TypeDefinition? def = td.Type as TypeDefinition; + + if (def is null && td.Type is TypeReference tr) + { + (string ns, string name) = tr.Names(); + + if (ns == "System" && name == "Guid") + { + return false; + } + + def = cache.Find(ns + "." + name); + } + + if (def is null) + { + return false; + } + + TypeCategory cat = TypeCategorization.GetCategory(def); + + if (cat != TypeCategory.Struct) + { + return false; + } + + // RequiresMarshaling, regardless of inner field layout. So for mapped types like + // Duration, KeyTime, RepeatBehavior (RequiresMarshaling=false), they're never "complex". + string sNs = td.Type?.Namespace?.Value ?? string.Empty; + string sName = td.Type?.Name?.Value ?? string.Empty; + MappedType? sMapped = MappedTypes.Get(sNs, sName); + + if (sMapped is not null) + { + return false; + } + + // A struct is "complex" if it has any field that is not a blittable primitive nor an + // almost-blittable struct (i.e. has a string/object/Nullable/etc. field). + foreach (FieldDefinition field in def.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + TypeSignature ft = field.Signature.FieldType; + + if (IsBlittablePrimitive(cache, ft)) + { + continue; + } + + if (IsAnyStruct(cache, ft)) + { + continue; + } + + return true; + } + return false; + } + + /// + /// Returns whether resolves to a struct type (mapped or user-defined). + /// + internal static bool IsAnyStruct(MetadataCache cache, TypeSignature sig) + { + if (sig is not TypeDefOrRefSignature td) + { + return false; + } + + TypeDefinition? def = td.Type as TypeDefinition; + + if (def is null && td.Type is TypeReference trEarly) + { + (string ns, string name) = trEarly.Names(); + + if (ns == "System" && name == "Guid") + { + return true; + } + + def = cache.Find(ns + "." + name); + } + + if (def is null) + { + return false; + } + + // Mapped struct types short-circuit based on the mapping's RequiresMarshaling flag + // (only applies to actual structs, not mapped interfaces like IAsyncAction). + if (TypeCategorization.GetCategory(def) == TypeCategory.Struct) + { + string sNs = td.Type?.Namespace?.Value ?? string.Empty; + string sName = td.Type?.Name?.Value ?? string.Empty; + MappedType? sMapped = MappedTypes.Get(sNs, sName); + + if (sMapped is { } sMappedVal) + { + return !sMappedVal.RequiresMarshaling; + } + } + + TypeCategory cat = TypeCategorization.GetCategory(def); + + if (cat != TypeCategory.Struct) + { + return false; + } + + // Reject if any instance field is a reference type (string/object/runtime class/etc.). + foreach (FieldDefinition field in def.Fields) + { + if (field.IsStatic || field.Signature is null) + { + continue; + } + + TypeSignature ft = field.Signature.FieldType; + + if (ft is CorLibTypeSignature corlibField) + { + if (corlibField.ElementType is + ElementType.String or + ElementType.Object) + { return false; } + continue; + } + + // Recurse: nested struct must also pass IsAnyStruct, otherwise reject. + if (IsBlittablePrimitive(cache, ft)) + { + continue; + } + + if (IsAnyStruct(cache, ft)) + { + continue; + } + + return false; + } + return true; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.MappedTypes.cs b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.MappedTypes.cs new file mode 100644 index 0000000000..eca5c68e7e --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.MappedTypes.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; +using static WindowsRuntime.ProjectionWriter.References.WellKnownTypeNames; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +internal static partial class AbiTypeHelpers +{ + /// + /// Returns the (possibly mapped) namespace of a type signature, or 'System' for fundamentals. + /// + internal static string GetMappedNamespace(TypeSignature sig) + { + // Fundamentals (string, bool, int, etc.) live in 'System' for ArrayMarshaller path purposes. + if (sig is CorLibTypeSignature) + { + return "System"; + } + + ITypeDefOrRef? td = null; + + if (sig is TypeDefOrRefSignature tds) + { + td = tds.Type; + } + else if (sig is GenericInstanceTypeSignature gi) + { + td = gi.GenericType; + } + + if (td is null) + { + return string.Empty; + } + + (string typeNs, string typeName) = td.Names(); + MappedType? mapped = MappedTypes.Get(typeNs, typeName); + return mapped is { } m ? m.MappedNamespace : typeNs; + } + + /// + /// True if the type is a mapped value type that requires marshalling between projected and ABI + /// representations (e.g. Windows.Foundation.DateTime <-> System.DateTimeOffset, + /// Windows.Foundation.TimeSpan <-> System.TimeSpan, Windows.Foundation.HResult <-> System.Exception). + /// These types use 'global::ABI.<MappedNamespace>.<MappedName>' as their ABI representation + /// and need an explicit marshaller call ('global::ABI.<MappedNamespace>.<MappedName>Marshaller.ConvertToUnmanaged'/ + /// 'ConvertToManaged') to convert values across the boundary. + /// + private static bool IsMappedMarshalingValueType(TypeSignature sig, out string mappedNs, out string mappedName) + { + mappedNs = string.Empty; + mappedName = string.Empty; + ITypeDefOrRef? td = null; + + if (sig is TypeDefOrRefSignature tds) + { + td = tds.Type; + } + + if (td is null) + { + return false; + } + + (string ns, string name) = td.Names(); + // The set of mapped types that use the 'value-type marshaller' pattern (DateTime, TimeSpan, HResult). + // Uri is also a mapped marshalling type but it's a reference type (handled via UriMarshaller separately). + if (ns == WindowsFoundation) + { + if (name == "DateTime") + { + mappedNs = "System"; mappedName = "DateTimeOffset"; return true; + } + + if (name == "TimeSpan") + { + mappedNs = "System"; mappedName = "TimeSpan"; return true; + } + + if (name == HResult) + { + mappedNs = "System"; mappedName = "Exception"; return true; + } + } + + return false; + } + + /// + /// True if the type is a mapped value type that needs ABI marshalling (excluding HResult, handled separately). + /// + internal static bool IsMappedAbiValueType(TypeSignature sig) + { + if (!IsMappedMarshalingValueType(sig, out _, out string mappedName)) + { + return false; + } + + // HResult/Exception is treated specially in many places; this helper is for DateTime/TimeSpan only. + return mappedName != "Exception"; + } + + /// + /// Returns the ABI type name for a mapped value type (e.g. 'global::ABI.System.TimeSpan'). + /// + internal static string GetMappedAbiTypeName(TypeSignature sig) + { + if (!IsMappedMarshalingValueType(sig, out string ns, out string name)) + { + return string.Empty; + } + + return GlobalAbiPrefix + ns + "." + name; + } + + /// + /// Returns the marshaller class name for a mapped value type (e.g. 'global::ABI.System.TimeSpanMarshaller'). + /// + internal static string GetMappedMarshallerName(TypeSignature sig) + { + if (!IsMappedMarshalingValueType(sig, out string ns, out string name)) + { + return string.Empty; + } + + return GlobalAbiPrefix + ns + "." + name + MarshallerSuffix; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.Marshallers.cs b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.Marshallers.cs new file mode 100644 index 0000000000..d7ba1543f4 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.Marshallers.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; +using static WindowsRuntime.ProjectionWriter.References.WellKnownTypeNames; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +internal static partial class AbiTypeHelpers +{ + /// True if the type signature is a Nullable<T> where T is a primitive + /// supported by an ABI.System.<T>Marshaller (e.g. UInt64Marshaller, Int32Marshaller, etc.). + /// Returns the fully-qualified marshaller name in . + internal static bool TryGetNullablePrimitiveMarshallerName(TypeSignature sig, out string? marshallerName) + { + marshallerName = null; + + if (sig is not GenericInstanceTypeSignature gi) + { + return false; + } + + ITypeDefOrRef gt = gi.GenericType; + string ns = gt?.Namespace?.Value ?? string.Empty; + string name = gt?.Name?.Value ?? string.Empty; + // In WinMD metadata, Nullable is encoded as Windows.Foundation.IReference. + // It only later gets projected to System.Nullable by the projection layer. + bool isNullable = (ns == "System" && name == NullableGeneric) + || (ns == WindowsFoundation && name == IReferenceGeneric); + + if (!isNullable) + { + return false; + } + + if (gi.TypeArguments.Count != 1) + { + return false; + } + + TypeSignature arg = gi.TypeArguments[0]; + // Map primitive corlib element type to its ABI marshaller name. + if (arg is CorLibTypeSignature corlib) + { + string? mn = corlib.ElementType switch + { + ElementType.Boolean => "Boolean", + ElementType.Char => "Char", + ElementType.I1 => "SByte", + ElementType.U1 => "Byte", + ElementType.I2 => "Int16", + ElementType.U2 => "UInt16", + ElementType.I4 => "Int32", + ElementType.U4 => "UInt32", + ElementType.I8 => "Int64", + ElementType.U8 => "UInt64", + ElementType.R4 => "Single", + ElementType.R8 => "Double", + _ => null + }; + + if (mn is null) + { + return false; + } + + marshallerName = AbiPrefix + "System." + mn + MarshallerSuffix; + return true; + } + + return false; + } + + /// Returns the marshaller name for the inner type T of Nullable<T>. + ///.: e.g. for Nullable<DateTimeOffset> returns + /// global::ABI.System.DateTimeOffsetMarshaller; for primitives like Nullable<int> + /// returns global::ABI.System.Int32Marshaller. + internal static string GetNullableInnerMarshallerName(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature innerType) + { + // Primitives (Int32, Int64, Boolean, etc.) live in ABI.System with the canonical .NET name. + if (innerType is CorLibTypeSignature corlib) + { + string typeName = corlib.ElementType switch + { + ElementType.Boolean => "Boolean", + ElementType.Char => "Char", + ElementType.I1 => "SByte", + ElementType.U1 => "Byte", + ElementType.I2 => "Int16", + ElementType.U2 => "UInt16", + ElementType.I4 => "Int32", + ElementType.U4 => "UInt32", + ElementType.I8 => "Int64", + ElementType.U8 => "UInt64", + ElementType.R4 => "Single", + ElementType.R8 => "Double", + _ => "", + }; + + if (!string.IsNullOrEmpty(typeName)) + { + return GlobalAbiPrefix + "System." + typeName + MarshallerSuffix; + } + } + + // For non-primitive types (DateTimeOffset, TimeSpan, struct/enum types), use GetMarshallerFullName. + return GetMarshallerFullName(writer, context, innerType); + } + + /// Returns the full marshaller name (e.g. global::ABI.Windows.Foundation.UriMarshaller). + /// When the marshaller would land in the writer's current ABI namespace, returns just the + /// short marshaller class name (e.g. BasicStructMarshaller) —. + /// elides the qualifier in same-namespace contexts. + internal static string GetMarshallerFullName(IndentedTextWriter writer, ProjectionEmitContext context, TypeSignature sig) + { + if (sig is TypeDefOrRefSignature td) + { + string ns = td.Type?.Namespace?.Value ?? string.Empty; + string name = td.Type?.Name?.Value ?? string.Empty; + // Apply mapped type remapping (e.g. System.Uri -> Windows.Foundation.Uri) + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + return GlobalAbiPrefix + ns + "." + IdentifierEscaping.StripBackticks(name) + MarshallerSuffix; + } + + return "global::ABI.Object.Marshaller"; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.cs b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.cs new file mode 100644 index 0000000000..ee5a670ab6 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AbiTypeHelpers.cs @@ -0,0 +1,444 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Globalization; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; +using WindowsRuntime.ProjectionWriter.Resolvers; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// ABI emission helpers for structs, enums, delegates, interfaces, and classes. +/// Provides predicates and writer helpers used by the per-kind ABI factories. +/// +internal static partial class AbiTypeHelpers +{ + /// + /// Returns the parent class for an interface marked [ExclusiveToAttribute(typeof(T))]. + /// + internal static TypeDefinition? GetExclusiveToType(MetadataCache cache, TypeDefinition iface) + { + 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 != WindowsFoundationMetadata || + attrType.Name?.Value != 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 fullName = sig.FullName ?? string.Empty; + TypeDefinition? td = cache.Find(fullName); + + 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; + } + + /// + /// Resolves an InterfaceImpl's interface reference to a TypeDefinition (same module or via metadata cache). + /// + internal static TypeDefinition? ResolveInterfaceTypeDef(MetadataCache cache, ITypeDefOrRef ifaceRef) + { + if (ifaceRef is TypeDefinition td) + { + return td; + } + + if (ifaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef? gen = gi.GenericType; + + if (gen is TypeDefinition gtd) + { + return gtd; + } + + if (gen is TypeReference gtr) + { + (string ns, string nm) = gtr.Names(); + return cache.Find(ns + "." + nm); + } + } + + if (ifaceRef is TypeReference tr) + { + (string ns, string nm) = tr.Names(); + return cache.Find(ns + "." + nm); + } + + return null; + } + + /// + /// Returns the unique virtual-method name used to refer to on + /// 's vtable: the method's metadata name suffixed with its zero-based + /// index in the type's method list, so overloads disambiguate (e.g. get_Item_4). + /// + /// The interface declaring the method. + /// The method whose vtable name to compute. + /// The virtual method name (name_index). + public static string GetVirtualMethodName(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(CultureInfo.InvariantCulture); + } + + /// + /// Returns the metadata-derived name for the return parameter, or the conventional + /// __return_value__ placeholder when the metadata does not name it. + /// + internal static string GetReturnParamName(MethodSignatureInfo sig) + { + string? n = sig.ReturnParameter?.Name?.Value; + + if (string.IsNullOrEmpty(n)) + { + return "__return_value__"; + } + + return CSharpKeywords.IsKeyword(n) ? "@" + n : n; + } + + /// + /// Returns the local-variable name for the return parameter on the server side. + /// abi_marshaler::get_marshaler_local() which prefixes __ to the param name. + /// + internal static string GetReturnLocalName(MethodSignatureInfo sig) + { + return "__" + GetReturnParamName(sig); + } + + /// + /// Returns '__<returnName>Size' — by default '____return_value__Size' for the standard '__return_value__' return param. + /// + internal static string GetReturnSizeParamName(MethodSignatureInfo sig) + { + return "__" + GetReturnParamName(sig) + "Size"; + } + + /// + /// Build a method-to-event map for add/remove accessors of a type. + /// + internal static Dictionary? BuildEventMethodMap(TypeDefinition type) + { + if (type.Events.Count == 0) + { + return null; + } + + Dictionary map = []; + 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; + } + + /// + /// Writes the IID GUID literal expression for the given runtime type (used by ABI emission paths). + /// + public static void WriteIidGuidReference(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + if (type.GenericParameters.Count != 0) + { + // Generic interface IID - call the unsafe accessor + IidExpressionGenerator.WriteIidGuidPropertyName(writer, context, type); + writer.Write("(null)"); + return; + } + + (string ns, string nm) = type.Names(); + + if (MappedTypes.Get(ns, nm) is { } m && m.MappedName == "IStringable") + { + writer.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable"); + return; + } + + writer.Write("global::ABI.InterfaceIIDs."); + IidExpressionGenerator.WriteIidGuidPropertyName(writer, context, type); + } + + /// + /// True if EmitNativeDelegateBody can emit a real (non-throw) body for this signature. + /// + internal static bool IsDelegateInvokeNativeSupported(MetadataCache cache, MethodSignatureInfo sig) + { + TypeSignature? rt = sig.ReturnType; + + if (rt is not null) + { + if (rt.IsHResultException()) + { + return false; + } + + if (!(IsBlittablePrimitive(cache, rt) || IsAnyStruct(cache, rt) || rt.IsString() || IsRuntimeClassOrInterface(cache, rt) || rt.IsObject() || rt.IsGenericInstance() || IsComplexStruct(cache, rt))) + { + return false; + } + } + + foreach (ParameterInfo p in sig.Parameters) + { + ParameterCategory cat = ParameterCategoryResolver.GetParamCategory(p); + + if (cat is ParameterCategory.PassArray or ParameterCategory.FillArray) + { + if (p.Type is SzArrayTypeSignature szP) + { + if (IsBlittablePrimitive(cache, szP.BaseType)) + { + continue; + } + + if (IsAnyStruct(cache, szP.BaseType)) + { + continue; + } + } + + return false; + } + + if (cat != ParameterCategory.In) + { + return false; + } + + if (p.Type.IsHResultException()) + { + return false; + } + + if (IsBlittablePrimitive(cache, p.Type)) + { + continue; + } + + if (IsAnyStruct(cache, p.Type)) + { + continue; + } + + if (p.Type.IsString()) + { + continue; + } + + if (IsRuntimeClassOrInterface(cache, p.Type)) + { + continue; + } + + if (p.Type.IsObject()) + { + continue; + } + + if (p.Type.IsGenericInstance()) + { + continue; + } + + if (IsComplexStruct(cache, p.Type)) + { + continue; + } + + return false; + } + + return true; + } + + /// + /// True if the interface has at least one non-special method, property, or non-skipped event. + /// + internal static bool HasEmittableMembers(TypeDefinition iface, bool skipExclusiveEvents) + { + foreach (MethodDefinition m in iface.Methods) + { + if (!m.IsSpecial()) + { + return true; + } + } + + if (iface.Properties.Count > 0) + { + return true; + } + + if (!skipExclusiveEvents && iface.Events.Count > 0) + { + return true; + } + + return false; + } + + /// + /// Returns the number of methods (including special accessors) on the interface. + /// + internal static int CountMethods(TypeDefinition iface) + { + return iface.Methods.Count; + } + + /// + /// Returns the number of base classes between and . + /// + internal static int GetClassHierarchyIndex(MetadataCache cache, TypeDefinition classType) + { + if (classType.BaseType is null) + { + return 0; + } + + (string ns, string nm) = classType.BaseType.Names(); + + if (ns == "System" && nm == "Object") + { + return 0; + } + + TypeDefinition? baseDef = classType.BaseType as TypeDefinition; + + if (baseDef is null) + { + baseDef = classType.BaseType.TryResolve(cache.RuntimeContext); + baseDef ??= cache.Find(string.IsNullOrEmpty(ns) ? nm : (ns + "." + nm)); + } + + if (baseDef is null) + { + return 0; + } + + return GetClassHierarchyIndex(cache, baseDef) + 1; + } + + /// + /// Returns whether two interface types refer to the same interface by namespace+name (used to compare interfaces across module boundaries). + /// + internal 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); + } + + /// Strips ByReferenceTypeSignature and CustomModifierTypeSignature wrappers + /// to get the underlying type signature. + internal static TypeSignature StripByRefAndCustomModifiers(TypeSignature sig) + { + TypeSignature current = sig; + while (true) + { + if (current is ByReferenceTypeSignature br) + { + current = br.BaseType; + continue; + } + + if (current is CustomModifierTypeSignature cm) + { + current = cm.BaseType; + continue; + } + + return current; + } + } + + /// + /// Returns the C#-callsite name for (the metadata name, escaped with + /// @ when it collides with a C# keyword). When is + /// non-, it replaces the metadata name. + /// + /// The parameter to name. + /// Optional override (FastAbi-merged Methods classes use this to inject a synthesized name). + /// The escaped parameter name. + internal static string GetParamName(ParameterInfo p, string? paramNameOverride) + { + string name = paramNameOverride ?? p.Parameter.Name ?? "param"; + return CSharpKeywords.IsKeyword(name) ? "@" + name : name; + } + + /// + /// Returns the local-variable name for (the metadata name without + /// the C#-keyword @ escape, since helper local names like __event are valid + /// even when the underlying parameter name is a C# keyword). + /// + /// The parameter to name. + /// Optional override. + /// The unescaped local-variable name. + internal static string GetParamLocalName(ParameterInfo p, string? paramNameOverride) + { + return paramNameOverride ?? p.Parameter.Name ?? "param"; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AbiTypeWriter.cs b/src/WinRT.Projection.Writer/Helpers/AbiTypeWriter.cs new file mode 100644 index 0000000000..ea7950afc5 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AbiTypeWriter.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; +using static WindowsRuntime.ProjectionWriter.References.WellKnownTypeNames; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Writes the ABI projection of a 'TypeSemantics' (or fundamental type) directly to an 'IndentedTextWriter'. +/// +internal static class AbiTypeWriter +{ + /// + /// Writes the C# representation of the ABI type for the given (e.g. fundamental primitives, void* for object/interface types, etc.). + /// + public static void WriteAbiType(IndentedTextWriter writer, ProjectionEmitContext context, TypeSemantics semantics) + { + switch (semantics) + { + case TypeSemantics.Fundamental f: + writer.Write(GetAbiFundamentalType(f.Type)); + break; + case TypeSemantics.ObjectType: + writer.Write("void*"); + break; + case TypeSemantics.GuidType: + writer.Write("Guid"); + break; + case TypeSemantics.SystemType: + writer.Write("global::WindowsRuntime.InteropServices.WindowsRuntimeTypeName"); + break; + case TypeSemantics.Definition d: + if (TypeCategorization.GetCategory(d.Type) is TypeCategory.Enum) + { + // Enums in WinRT ABI use the projected enum type directly (since their C# + // layout matches their underlying integer ABI representation 1:1). + TypedefNameWriter.WriteTypedefName(writer, context, d.Type, TypedefNameType.Projected, true); + } + else if (TypeCategorization.GetCategory(d.Type) is TypeCategory.Struct) + { + (string dNs, string dName) = d.Type.Names(); + // Special case: mapped value types that require ABI marshalling + // (DateTime/TimeSpan -> ABI.System.DateTimeOffset/TimeSpan). + if (dNs == WindowsFoundation && dName == "DateTime") + { + writer.Write("global::ABI.System.DateTimeOffset"); + break; + } + + if (dNs == WindowsFoundation && dName == "TimeSpan") + { + writer.Write("global::ABI.System.TimeSpan"); + break; + } + + if (dNs == WindowsFoundation && dName == HResult) + { + writer.Write("global::ABI.System.Exception"); + break; + } + + if (dNs == WindowsUIXamlInterop && dName == TypeName) + { + // System.Type ABI struct: maps to global::ABI.System.Type, not the + // ABI.Windows.UI.Xaml.Interop.TypeName form. + writer.Write("global::ABI.System.Type"); + break; + } + + TypeSignature dts = d.Type.ToTypeSignature(); + // "Almost-blittable" structs (with bool/char fields but no reference-type + // fields) can pass through using the projected type since the C# layout + // matches the WinRT ABI directly. Truly complex structs (with string/object/ + // Nullable fields) need the ABI struct. + if (context.AbiTypeShapeResolver.IsBlittableStruct(dts)) + { + TypedefNameWriter.WriteTypedefName(writer, context, d.Type, TypedefNameType.Projected, true); + } + else + { + TypedefNameWriter.WriteTypedefName(writer, context, d.Type, TypedefNameType.ABI, true); + } + } + else + { + writer.Write("void*"); + } + + break; + case TypeSemantics.Reference r: + // Cross-module typeref: try resolving the type, applying mapped-type translation + // for the field/parameter type after resolution. + if (context.Cache is not null) + { + (string rns, string rname) = r.Type.Names(); + // Special case: mapped value types that require ABI marshalling. + if (rns == WindowsFoundation && rname == "DateTime") + { + writer.Write("global::ABI.System.DateTimeOffset"); + break; + } + + if (rns == WindowsFoundation && rname == "TimeSpan") + { + writer.Write("global::ABI.System.TimeSpan"); + break; + } + + if (rns == WindowsFoundation && rname == HResult) + { + writer.Write("global::ABI.System.Exception"); + break; + } + + // Look up the type by its ORIGINAL (unmapped) name in the cache. + TypeDefinition? rd = context.Cache.Find(rns + "." + rname); + // If not found, try the mapped name (for cases where the mapping target is in the cache). + if (rd is null) + { + if (MappedTypes.Get(rns, rname) is { } rmapped) + { + rd = context.Cache.Find(rmapped.MappedNamespace + "." + rmapped.MappedName); + } + } + + if (rd is not null) + { + TypeCategory cat = TypeCategorization.GetCategory(rd); + + if (cat == TypeCategory.Enum) + { + // Enums use the projected enum type directly (C# layout == ABI layout). + TypedefNameWriter.WriteTypedefName(writer, context, rd, TypedefNameType.Projected, true); + break; + } + + if (cat == TypeCategory.Struct) + { + // Special case: HResult is mapped to System.Exception (a reference type) + // but its ABI representation is the global::ABI.System.Exception struct + // (which wraps the underlying HRESULT int). + (string rdNs, string rdName) = rd.Names(); + + if (rdNs == WindowsFoundation && rdName == HResult) + { + writer.Write("global::ABI.System.Exception"); + break; + } + + if (context.AbiTypeShapeResolver.IsBlittableStruct(rd.ToTypeSignature())) + { + TypedefNameWriter.WriteTypedefName(writer, context, rd, TypedefNameType.Projected, true); + } + else + { + TypedefNameWriter.WriteTypedefName(writer, context, rd, TypedefNameType.ABI, true); + } + + break; + } + } + } + + // Unresolved cross-assembly TypeRef. If the signature was encoded as a value type + // (e.g. WindowId from Microsoft.UI.winmd when that winmd isn't loaded), assume it's + // a blittable struct and emit the projected type name — the consumer's compiler + // will resolve it via their own references. Otherwise (encoded as Class) emit + // void* (it's a runtime class/interface/delegate). + if (r.IsValueType) + { + (string rns, string rname) = r.Type.Names(); + writer.Write(GlobalPrefix); + + if (!string.IsNullOrEmpty(rns)) + { + writer.Write($"{rns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(rname)); + break; + } + + writer.Write("void*"); + break; + case TypeSemantics.GenericInstance: + writer.Write("void*"); + break; + default: + writer.Write("void*"); + break; + } + } + + /// + /// Returns the ABI C# type name for a fundamental type (e.g. "bool" -> "byte", "char" -> "ushort"). + /// + internal static string GetAbiFundamentalType(FundamentalType t) => t switch + { + FundamentalType.Boolean => "bool", + FundamentalType.Char => "char", + FundamentalType.String => "void*", + _ => FundamentalTypes.ToCSharpType(t) + }; +} diff --git a/src/WinRT.Projection.Writer/Helpers/AdditionTypes.cs b/src/WinRT.Projection.Writer/Helpers/AdditionTypes.cs new file mode 100644 index 0000000000..7162940aa0 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AdditionTypes.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Static lookup for the (namespace, type-name) pairs that have an associated namespace-additions +/// resource. Used by the projected-struct emitter to decide whether the struct should be projected +/// as partial (so the addition file's content can extend it). +/// +internal static class AdditionTypes +{ + private static readonly FrozenDictionary> Table = new Dictionary>(StringComparer.Ordinal) + { + ["Microsoft.UI.Xaml"] = FrozenSet.Create(StringComparer.Ordinal, "Thickness"), + ["Microsoft.UI.Xaml.Controls.Primitives"] = FrozenSet.Create(StringComparer.Ordinal, "GeneratorPosition"), + ["Microsoft.UI.Xaml.Media"] = FrozenSet.Create(StringComparer.Ordinal, "Matrix"), + ["Microsoft.UI.Xaml.Media.Animation"] = FrozenSet.Create(StringComparer.Ordinal, "KeyTime"), + ["Windows.UI"] = FrozenSet.Create(StringComparer.Ordinal, "Color"), + ["Windows.UI.Xaml"] = FrozenSet.Create(StringComparer.Ordinal, "Thickness"), + ["Windows.UI.Xaml.Controls.Primitives"] = FrozenSet.Create(StringComparer.Ordinal, "GeneratorPosition"), + ["Windows.UI.Xaml.Media"] = FrozenSet.Create(StringComparer.Ordinal, "Matrix"), + ["Windows.UI.Xaml.Media.Animation"] = FrozenSet.Create(StringComparer.Ordinal, "KeyTime"), + }.ToFrozenDictionary(StringComparer.Ordinal); + + /// + /// Returns whether the type identified by (, ) + /// has an associated namespace-additions resource. + /// + /// The type's namespace. + /// The type's short name (without generic arity). + /// if an addition exists; otherwise . + public static bool HasAdditionToType(string typeNamespace, string typeName) + { + return Table.TryGetValue(typeNamespace, out FrozenSet? names) && names.Contains(typeName); + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/Additions.cs b/src/WinRT.Projection.Writer/Helpers/Additions.cs new file mode 100644 index 0000000000..e782a1c7d4 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/Additions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Registry of namespace addition files. +/// 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 = + [ + ("Microsoft.UI.Dispatching", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Dispatching.Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.CornerRadius.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.Duration.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.GridLength.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.SR.cs"), + ("Microsoft.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.Thickness.cs"), + ("Microsoft.UI.Xaml.Controls.Primitives", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Controls.Primitives.Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs"), + ("Microsoft.UI.Xaml.Media", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Media.Microsoft.UI.Xaml.Media.Matrix.cs"), + ("Microsoft.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Media.Animation.Microsoft.UI.Xaml.Media.Animation.KeyTime.cs"), + ("Microsoft.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Media.Animation.Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs"), + ("Microsoft.UI.Xaml.Media.Media3D", "WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Media.Media3D.Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs"), + ("Windows.Storage", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.Storage.WindowsRuntimeStorageExtensions.cs"), + ("Windows.UI", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Windows.UI.Color.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.System.DispatcherQueueSynchronizationContext.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.CornerRadius.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.Duration.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.GridLength.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.SR.cs"), + ("Windows.UI.Xaml", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.Thickness.cs"), + ("Windows.UI.Xaml.Controls.Primitives", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Controls.Primitives.Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs"), + ("Windows.UI.Xaml.Media", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Media.Windows.UI.Xaml.Media.Matrix.cs"), + ("Windows.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Media.Animation.Windows.UI.Xaml.Media.Animation.KeyTime.cs"), + ("Windows.UI.Xaml.Media.Animation", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Media.Animation.Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs"), + ("Windows.UI.Xaml.Media.Media3D", "WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Media.Media3D.Windows.UI.Xaml.Media.Media3D.Matrix3D.cs"), + ]; + + /// + /// Lookup of the manifest resource names for a given target namespace, in the same order they + /// appear in . Lookups against this map replace per-namespace linear scans of + /// in the per-namespace emission path. + /// + public static readonly FrozenDictionary ByNamespace = + All.GroupBy(static x => x.Namespace) + .ToFrozenDictionary(static g => g.Key, static g => g.Select(x => x.ResourceName).ToArray()); +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/ArrayElementEncoder.cs b/src/WinRT.Projection.Writer/Helpers/ArrayElementEncoder.cs new file mode 100644 index 0000000000..549823141e --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/ArrayElementEncoder.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Text; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Metadata; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Encodes WinRT array element type names for use in ABI marshaller paths (e.g. 'Int32', 'NullableUInt32', 'Single_RGB_BlueGreenRed_Iface'). +/// +internal static class ArrayElementEncoder +{ + /// + /// 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). + /// + internal static string GetArrayMarshallerInteropPath(TypeSignature elementType) + { + // 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 = AbiTypeHelpers.GetMappedNamespace(elementType); + + if (string.IsNullOrEmpty(ns)) + { + return "ABI.<" + topLevelElement + ">ArrayMarshaller, WinRT.Interop"; + } + + return "ABI." + ns + ".<" + topLevelElement + ">ArrayMarshaller, WinRT.Interop"; + } + + /// + /// 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(TypeSignature elementType) + { + System.Text.StringBuilder sb = new(); + EncodeArrayElementNameInto(sb, elementType); + return sb.ToString(); + } + + private static void EncodeArrayElementNameInto(System.Text.StringBuilder sb, TypeSignature sig) + { + // Special case for System.Guid: 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 TypeDefOrRefSignature gtd + && gtd.Type?.Namespace?.Value == "System" + && gtd.Type?.Name?.Value == "Guid") + { + _ = sb.Append("<#corlib>Guid"); + return; + } + + switch (sig) + { + case CorLibTypeSignature corlib: + InteropTypeNameWriter.EncodeFundamental(sb, corlib, TypedefNameType.Projected); + return; + case TypeDefOrRefSignature td: + EncodeArrayElementForTypeDef(sb, td.Type, generic_args: null); + return; + case GenericInstanceTypeSignature gi: + EncodeArrayElementForTypeDef(sb, gi.GenericType, generic_args: gi.TypeArguments); + return; + default: + _ = sb.Append(sig.FullName); + return; + } + } + + private static void EncodeArrayElementForTypeDef(StringBuilder sb, ITypeDefOrRef type, IList? generic_args) + { + (string typeNs, string typeName) = type.Names(); + // Apply mapped-type remapping (e.g. Windows.Foundation.IReference -> System.Nullable). + MappedType? mapped = MappedTypes.Get(typeNs, typeName); + + if (mapped is { } m) + { + typeNs = m.MappedNamespace; + typeName = m.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(InteropTypeNameWriter.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('|'); + } + + InteropTypeNameWriter.EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); + } + _ = sb.Append('>'); + } + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/AttributedTypes.cs b/src/WinRT.Projection.Writer/Helpers/AttributedTypes.cs new file mode 100644 index 0000000000..0c1529cdce --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/AttributedTypes.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Metadata; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Information about an [Activatable]/[Static]/[Composable] factory interface. +/// +/// The factory-interface type definition (or when the attribute did not carry one). +/// Whether the carrying type has an [Activatable] attribute pointing at . +/// Whether the carrying type has a [Static] attribute pointing at . +/// Whether the carrying type has a [Composable] attribute pointing at . +/// For composable attributes, whether the visibility argument is the public (2) value. +internal sealed record AttributedType( + TypeDefinition? Type = null, + bool Activatable = false, + bool Statics = false, + bool Composable = false, + bool Visible = false); + +/// +/// Helpers for activator/static/composable factory enumeration. +/// +internal static class AttributedTypes +{ + /// + /// Returns the (interface_name, AttributedType) entries for the given runtime class type. + /// + public static IEnumerable> Get(TypeDefinition type, MetadataCache cache) + { + Dictionary result = []; + + 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, string name) = attrType.Names(); + + if (ns != WindowsFoundationMetadata) + { + continue; + } + + AttributedType info; + switch (name) + { + case ActivatableAttribute: + info = new AttributedType(Type: GetSystemType(attr, cache), Activatable: true); + break; + case StaticAttribute: + info = new AttributedType(Type: GetSystemType(attr, cache), Statics: true); + break; + case ComposableAttribute: + info = new AttributedType(Type: GetSystemType(attr, cache), Composable: true, Visible: GetVisibility(attr) == 2); + break; + default: + continue; + } + + string key = info.Type?.Name?.Value ?? string.Empty; + result[key] = info; + } + + // Sort by key (the factory-interface type name, e.g. 'IButtonUtilsStatic') so the + // inheritance order in the generated code is alphabetical by interface name. + SortedDictionary sorted = []; + 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; + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/CSharpKeywords.cs b/src/WinRT.Projection.Writer/Helpers/CSharpKeywords.cs new file mode 100644 index 0000000000..b03e9ca4fa --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/CSharpKeywords.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Frozen; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Recognizes C# language keywords. +/// +internal static class CSharpKeywords +{ + private static readonly FrozenSet Keywords = new[] + { + "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" + }.ToFrozenSet(); + + /// + /// Returns whether is a reserved C# language keyword. + /// + /// The identifier to test. + /// if is a C# keyword; otherwise . + public static bool IsKeyword(string identifier) => Keywords.Contains(identifier); +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/ContractPlatforms.cs b/src/WinRT.Projection.Writer/Helpers/ContractPlatforms.cs new file mode 100644 index 0000000000..d4667a180d --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/ContractPlatforms.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Maps Windows Runtime API contracts to their first available Windows SDK platform version. +/// +internal static class ContractPlatforms +{ + private static readonly FrozenDictionary 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 (!Table.TryGetValue(contractName, out (int Version, string Platform)[]? versions)) + { + return string.Empty; + } + + // Find the first version >= contractVersion. + for (int i = 0; i < versions.Length; i++) + { + if (versions[i].Version >= contractVersion) + { + return versions[i].Platform; + } + } + return string.Empty; + } + + private static FrozenDictionary Build() + { + Dictionary t = new(StringComparer.Ordinal) + { + ["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")], + ["Windows.AI.MachineLearning.Preview.MachineLearningPreviewContract"] = + [(1, "10.0.17134.0"), (2, "10.0.17763.0")], + ["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")], + ["Windows.ApplicationModel.Calls.CallsPhoneContract"] = + [(4, "10.0.17763.0"), (5, "10.0.18362.0"), (6, "10.0.20348.0"), (7, "10.0.22621.0")], + ["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")], + ["Windows.ApplicationModel.CommunicationBlocking.CommunicationBlockingContract"] = + [(2, "10.0.17763.0")], + ["Windows.ApplicationModel.SocialInfo.SocialInfoContract"] = + [(1, "10.0.14393.0"), (2, "10.0.15063.0")], + ["Windows.ApplicationModel.StartupTaskContract"] = + [(2, "10.0.16299.0"), (3, "10.0.17134.0")], + ["Windows.Devices.Custom.CustomDeviceContract"] = + [(1, "10.0.16299.0")], + ["Windows.Devices.DevicesLowLevelContract"] = + [(2, "10.0.14393.0"), (3, "10.0.15063.0")], + ["Windows.Devices.Printers.PrintersContract"] = + [(1, "10.0.10586.0")], + ["Windows.Devices.SmartCards.SmartCardBackgroundTriggerContract"] = + [(3, "10.0.16299.0")], + ["Windows.Devices.SmartCards.SmartCardEmulatorContract"] = + [(5, "10.0.16299.0"), (6, "10.0.17763.0")], + ["Windows.Foundation.FoundationContract"] = + [(1, "10.0.10240.0"), (2, "10.0.10586.0"), (3, "10.0.15063.0"), (4, "10.0.19041.0")], + ["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")], + ["Windows.Foundation.VelocityIntegration.VelocityIntegrationContract"] = + [(1, "10.0.17134.0")], + ["Windows.Gaming.XboxLive.StorageApiContract"] = + [(1, "10.0.16299.0")], + ["Windows.Graphics.Printing3D.Printing3DContract"] = + [(2, "10.0.10586.0"), (3, "10.0.14393.0"), (4, "10.0.16299.0")], + ["Windows.Networking.Connectivity.WwanContract"] = + [(1, "10.0.10240.0"), (2, "10.0.17134.0"), (3, "10.0.26100.0")], + ["Windows.Networking.Sockets.ControlChannelTriggerContract"] = + [(3, "10.0.17763.0")], + ["Windows.Security.Isolation.IsolatedWindowsEnvironmentContract"] = + [(1, "10.0.19041.0"), (3, "10.0.20348.0"), (4, "10.0.22621.0"), (5, "10.0.26100.0")], + ["Windows.Services.Maps.GuidanceContract"] = + [(3, "10.0.17763.0")], + ["Windows.Services.Maps.LocalSearchContract"] = + [(4, "10.0.17763.0")], + ["Windows.Services.Store.StoreContract"] = + [(1, "10.0.14393.0"), (2, "10.0.15063.0"), (3, "10.0.17134.0"), (4, "10.0.17763.0")], + ["Windows.Services.TargetedContent.TargetedContentContract"] = + [(1, "10.0.15063.0")], + ["Windows.Storage.Provider.CloudFilesContract"] = + [(4, "10.0.19041.0"), (6, "10.0.20348.0"), (7, "10.0.22621.0")], + ["Windows.System.Profile.ProfileHardwareTokenContract"] = + [(1, "10.0.14393.0")], + ["Windows.System.Profile.ProfileRetailInfoContract"] = + [(1, "10.0.20348.0")], + ["Windows.System.Profile.ProfileSharedModeContract"] = + [(1, "10.0.14393.0"), (2, "10.0.15063.0")], + ["Windows.System.Profile.SystemManufacturers.SystemManufacturersContract"] = + [(3, "10.0.17763.0")], + ["Windows.System.SystemManagementContract"] = + [(6, "10.0.17763.0"), (7, "10.0.19041.0")], + ["Windows.UI.UIAutomation.UIAutomationContract"] = + [(1, "10.0.20348.0"), (2, "10.0.22000.0")], + ["Windows.UI.ViewManagement.ViewManagementViewScalingContract"] = + [(1, "10.0.14393.0")], + ["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.ToFrozenDictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/GuidGenerator.cs b/src/WinRT.Projection.Writer/Helpers/GuidGenerator.cs new file mode 100644 index 0000000000..768f5769cb --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/GuidGenerator.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Generates Windows Runtime parameterized GUIDs (PIIDs) by combining the WinRT-defined +/// namespace GUID (d57af411-737b-c042-abae-878b1e16adee) with the type's signature +/// hashed via SHA-1 (per the WinRT type-system spec). +/// +internal static class GuidGenerator +{ + /// + /// The PIID for the Windows Runtime namespace, used for generating IIDs of generic instantiations. + /// + /// + private static readonly Guid WindowsRuntimePiidNamespace = new(0xD57AF411, 0x737B, 0xC042, 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(ReadOnlySpan signature) + { + // Stack-allocate the UTF-8 buffer (namespace GUID prefix + signature bytes) when possible; + // fall back to ArrayPool for very long signatures. The 512-byte threshold matches interop's + // GuidGenerator and is large enough for virtually every realistic signature. + int maxUtf8ByteCount = Encoding.UTF8.GetMaxByteCount(signature.Length); + int minimumPooledLength = 16 + maxUtf8ByteCount; + byte[]? utf8BytesFromPool = null; + Span utf8Bytes = minimumPooledLength <= 512 + ? stackalloc byte[512] + : (utf8BytesFromPool = ArrayPool.Shared.Rent(minimumPooledLength)); + + _ = WindowsRuntimePiidNamespace.TryWriteBytes(utf8Bytes); + + int encodedUtf8BytesWritten = Encoding.UTF8.GetBytes(signature, utf8Bytes[16..]); + Span sha1Bytes = stackalloc byte[SHA1.HashSizeInBytes]; + + _ = SHA1.HashData(utf8Bytes[..(16 + encodedUtf8BytesWritten)], sha1Bytes); + + Guid iid = EncodeGuid(sha1Bytes); + + if (utf8BytesFromPool is not null) + { + ArrayPool.Shared.Return(utf8BytesFromPool); + } + + return iid; + } + + /// + /// Encodes a from the first 16 bytes of following + /// RFC 4122 rules: applies the little-endian byte-order swaps for the int and two + /// short fields, sets the version (5) and variant (RFC 4122) bits. + /// + /// The input span (must be at least 16 bytes). + /// The resulting GUID. + private static Guid EncodeGuid(ReadOnlySpan data) + { + Debug.Assert(data.Length >= 16); + + Span buffer = stackalloc byte[16]; + + data[..16].CopyTo(buffer); + + if (BitConverter.IsLittleEndian) + { + // Swap bytes of 'int a' + (buffer[3], buffer[0]) = (buffer[0], buffer[3]); + (buffer[2], buffer[1]) = (buffer[1], buffer[2]); + + // Swap bytes of 'short b' + (buffer[5], buffer[4]) = (buffer[4], buffer[5]); + + // Swap bytes of 'short c' and encode the RFC time/version field + (buffer[7], buffer[6]) = ((byte)((buffer[6] & 0x0F) | (5 << 4)), buffer[7]); + + // Encode the RFC clock/reserved field + buffer[8] = (byte)((buffer[8] & 0x3F) | 0x80); + } + + return new(buffer); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/IdentifierEscaping.cs b/src/WinRT.Projection.Writer/Helpers/IdentifierEscaping.cs new file mode 100644 index 0000000000..a1798b2155 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/IdentifierEscaping.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Helpers for converting raw metadata names into valid C# identifiers. +/// +internal static class IdentifierEscaping +{ + /// + /// Strips a generic-arity backtick suffix from a metadata type name (e.g. "IList`1" + /// becomes "IList"). + /// + /// The metadata type name to strip. + /// The type name without its backtick suffix. + public static string StripBackticks(string typeName) + { + int idx = typeName.IndexOf('`'); + return idx >= 0 ? typeName[..idx] : typeName; + } + + /// + /// Writes to , prefixed with @ + /// if it is a reserved C# keyword. + /// + /// The writer to emit to. + /// The identifier to write. + public static void WriteEscapedIdentifier(IndentedTextWriter writer, string identifier) + { + if (CSharpKeywords.IsKeyword(identifier)) + { + writer.Write("@"); + } + + writer.Write(identifier); + } + + /// + /// Returns the camel-case form of : if the first character is an + /// upper-case ASCII letter, it is lowered; otherwise is returned + /// unchanged. Used to derive C# constructor parameter names from public field names. + /// + /// The name to lower-case the first character of. + /// The camel-case form. + public static string ToCamelCase(string name) + { + if (string.IsNullOrEmpty(name)) + { + return name; + } + + char c = name[0]; + + if (c is >= 'A' and <= 'Z') + { + return char.ToLowerInvariant(c) + name[1..]; + } + + return name; + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/IidExpressionGenerator.cs b/src/WinRT.Projection.Writer/Helpers/IidExpressionGenerator.cs new file mode 100644 index 0000000000..0b2afafa79 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/IidExpressionGenerator.cs @@ -0,0 +1,534 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Helpers for emitting WinRT GUID / IID expressions: signature characters for the GUID +/// hash algorithm, the canonical hyphenated string form of a type's [Guid], and the +/// byte-list form used when initializing native IID storage. +/// +internal static partial class IidExpressionGenerator +{ + /// + /// Returns the GUID-signature character code for a fundamental WinRT type. + /// + public static string GetFundamentalTypeGuidSignature(FundamentalType t) => t switch + { + FundamentalType.Boolean => "b1", + FundamentalType.Char => "c2", + FundamentalType.Int8 => "i1", + FundamentalType.UInt8 => "u1", + FundamentalType.Int16 => "i2", + FundamentalType.UInt16 => "u2", + FundamentalType.Int32 => "i4", + FundamentalType.UInt32 => "u4", + FundamentalType.Int64 => "i8", + FundamentalType.UInt64 => "u8", + FundamentalType.Float => "f4", + FundamentalType.Double => "f8", + FundamentalType.String => "string", + _ => throw WellKnownProjectionWriterExceptions.UnknownFundamentalType() + }; + + [GeneratedRegex(@"[ :<>`,.]")] + private static partial Regex TypeNameEscapeRegex(); + + /// + /// Escapes a type name into a C# identifier-safe form. + /// + public static string EscapeTypeNameForIdentifier(string typeName, bool stripGlobal = false, bool stripGlobalABI = false) + { + // Escape special chars first, then strip ONLY the prefix (not all occurrences). + string result = TypeNameEscapeRegex().Replace(typeName, "_"); + + if (stripGlobalABI && typeName.StartsWith(GlobalAbiPrefix, StringComparison.Ordinal)) + { + result = result[12..]; // Remove GlobalAbiPrefix (with ":" and "." already replaced) + } + else if (stripGlobal && typeName.StartsWith(GlobalPrefix, StringComparison.Ordinal)) + { + result = result[8..]; // Remove GlobalPrefix + } + + return result; + } + + /// + /// Reads the GUID values from the [GuidAttribute] of the type and returns them as a tuple. + /// + public static (uint Data1, ushort Data2, ushort Data3, byte[] Data4)? GetGuidFields(TypeDefinition type) + { + CustomAttribute? attr = type.GetAttribute(WindowsFoundationMetadata, "GuidAttribute"); + + if (attr is null || attr.Signature is null) + { + return null; + } + + IList args = attr.Signature.FixedArguments; + + if (args.Count < 11) + { + return null; + } + + uint data1 = ToUInt32(args[0].Element); + ushort data2 = ToUInt16(args[1].Element); + ushort data3 = ToUInt16(args[2].Element); + byte[] data4 = new byte[8]; + for (int i = 0; i < 8; i++) + { + data4[i] = ToByte(args[3 + i].Element); + } + return (data1, data2, data3, data4); + + static uint ToUInt32(object? v) => v switch + { + uint u => u, + int i => (uint)i, + _ => 0u + }; + static ushort ToUInt16(object? v) => v switch + { + ushort u => u, + short s => (ushort)s, + int i => (ushort)i, + _ => 0 + }; + static byte ToByte(object? v) => v switch + { + byte b => b, + sbyte sb => (byte)sb, + int i => (byte)i, + _ => 0 + }; + } + + /// + /// Writes the GUID for in canonical hyphenated string form. + /// + public static void WriteGuid(IndentedTextWriter writer, TypeDefinition type, bool lowerCase) + { + (uint data1, ushort data2, ushort data3, byte[] data4) = GetGuidFields(type) ?? throw WellKnownProjectionWriterExceptions.MissingGuidAttribute($"{type.Namespace}.{type.Name}"); + string fmt = lowerCase ? "x" : "X"; + // Format: %08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x + writer.Write($"{data1.ToString(fmt + "8", CultureInfo.InvariantCulture)}-{data2.ToString(fmt + "4", CultureInfo.InvariantCulture)}-{data3.ToString(fmt + "4", CultureInfo.InvariantCulture)}-"); + + for (int i = 0; i < 2; i++) + { + writer.Write(data4[i].ToString(fmt + "2", CultureInfo.InvariantCulture)); + } + + writer.Write("-"); + + for (int i = 2; i < 8; i++) + { + writer.Write(data4[i].ToString(fmt + "2", CultureInfo.InvariantCulture)); + } + } + + /// + /// Writes the GUID bytes for as a hex byte list. + /// + public static void WriteGuidBytes(IndentedTextWriter writer, TypeDefinition type) + { + (uint data1, ushort data2, ushort data3, byte[] data4) = GetGuidFields(type) ?? throw WellKnownProjectionWriterExceptions.MissingGuidAttribute($"{type.Namespace}.{type.Name}"); + WriteByte(writer, (data1 >> 0) & 0xFF, true); + WriteByte(writer, (data1 >> 8) & 0xFF, false); + WriteByte(writer, (data1 >> 16) & 0xFF, false); + WriteByte(writer, (data1 >> 24) & 0xFF, false); + WriteByte(writer, (uint)((data2 >> 0) & 0xFF), false); + WriteByte(writer, (uint)((data2 >> 8) & 0xFF), false); + WriteByte(writer, (uint)((data3 >> 0) & 0xFF), false); + WriteByte(writer, (uint)((data3 >> 8) & 0xFF), false); + + for (int i = 0; i < 8; i++) + { + WriteByte(writer, data4[i], false); + } + } + private static void WriteByte(IndentedTextWriter writer, uint b, bool first) + { + if (!first) + { + writer.Write(", "); + } + + writer.Write($"0x{(b & 0xFF).ToString("X", CultureInfo.InvariantCulture)}"); + } + + /// + /// Writes the property name IID_X for the IID property of . + /// + public static void WriteIidGuidPropertyName(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = EscapeTypeNameForIdentifier(TypedefNameWriter.WriteTypedefNameWithTypeParams(context, type, TypedefNameType.ABI, true), true, true); + writer.Write($"IID_{name}"); + } + + /// + /// Writes the property name IID_XReference for the reference IID property. + /// + public static void WriteIidReferenceGuidPropertyName(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string name = EscapeTypeNameForIdentifier(TypedefNameWriter.WriteTypedefNameWithTypeParams(context, type, TypedefNameType.ABI, true), true, true); + writer.Write($"IID_{name}Reference"); + } + + /// + /// Writes a static IID property whose body is built from the [Guid] attribute bytes. + /// + public static void WriteIidGuidPropertyFromType(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + writer.Write("public static ref readonly Guid "); + WriteIidGuidPropertyName(writer, context, type); + writer.WriteLine(); + writer.Write(""" + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + + """, isMultiline: true); + WriteGuidBytes(writer, type); + writer.WriteLine(); + writer.WriteLine(""" + ]; + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + """, isMultiline: true); + writer.WriteLine(); + } + + /// + /// Writes the WinRT GUID parametric signature string for a type semantics. + /// + public static void WriteGuidSignature(IndentedTextWriter writer, ProjectionEmitContext context, TypeSemantics semantics) + { + switch (semantics) + { + case TypeSemantics.GuidType: + writer.Write("g16"); + break; + case TypeSemantics.ObjectType: + writer.Write("cinterface(IInspectable)"); + break; + case TypeSemantics.Fundamental f: + writer.Write(GetFundamentalTypeGuidSignature(f.Type)); + break; + case TypeSemantics.Definition d: + WriteGuidSignatureForType(writer, context, d.Type); + break; + case TypeSemantics.Reference r: + { + // Resolve the reference to a TypeDefinition (cross-module struct field, etc.). + (string ns, string name) = r.Type.Names(); + TypeDefinition? resolved = null; + + if (context.Cache is not null) + { + resolved = r.Type.TryResolve(context.Cache.RuntimeContext) + ?? context.Cache.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); + } + + if (resolved is not null) + { + WriteGuidSignatureForType(writer, context, resolved); + } + } + break; + case TypeSemantics.GenericInstance gi: + writer.Write("pinterface({"); + WriteGuid(writer, gi.GenericType, true); + writer.Write("};"); + for (int i = 0; i < gi.GenericArgs.Count; i++) + { + if (i > 0) + { + writer.Write(";"); + } + + WriteGuidSignature(writer, context, gi.GenericArgs[i]); + } + writer.Write(")"); + break; + case TypeSemantics.GenericInstanceRef gir: + { + // Cross-module generic instance (e.g. Windows.Foundation.IReference + // appearing as a struct field). Resolve the generic type to a TypeDefinition + // so we can extract its [Guid]; recurse on each type argument. + (string ns, string name) = gir.GenericType.Names(); + TypeDefinition? resolved = null; + + if (context.Cache is not null) + { + resolved = gir.GenericType.TryResolve(context.Cache.RuntimeContext) + ?? context.Cache.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); + } + + if (resolved is not null) + { + writer.Write("pinterface({"); + WriteGuid(writer, resolved, true); + writer.Write("};"); + for (int i = 0; i < gir.GenericArgs.Count; i++) + { + if (i > 0) + { + writer.Write(";"); + } + + WriteGuidSignature(writer, context, gir.GenericArgs[i]); + } + writer.Write(")"); + } + } + break; + } + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the GUID signature into it, and returns the resulting string. + /// + /// The active emit context. + /// The type semantics whose GUID signature is emitted. + /// The emitted GUID signature. + public static string WriteGuidSignature(ProjectionEmitContext context, TypeSemantics semantics) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteGuidSignature(writer, context, semantics); + return writer.ToString(); + } + + private static void WriteGuidSignatureForType(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + TypeCategory cat = TypeCategorization.GetCategory(type); + switch (cat) + { + case TypeCategory.Enum: + writer.Write("enum("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.NonProjected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(";"); + writer.Write(TypeCategorization.IsFlagsEnum(type) ? "u4" : "i4"); + writer.Write(")"); + break; + case TypeCategory.Struct: + writer.Write("struct("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.NonProjected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(";"); + bool first = true; + foreach (FieldDefinition field in type.Fields) + { + if (field.IsStatic) + { + continue; + } + + if (field.Signature is null) + { + continue; + } + + if (!first) + { + writer.Write(";"); + } + + first = false; + WriteGuidSignature(writer, context, TypeSemanticsFactory.Get(field.Signature.FieldType)); + } + writer.Write(")"); + break; + case TypeCategory.Delegate: + writer.Write("delegate({"); + WriteGuid(writer, type, true); + writer.Write("})"); + break; + case TypeCategory.Interface: + writer.Write("{"); + WriteGuid(writer, type, true); + writer.Write("}"); + break; + case TypeCategory.Class: + ITypeDefOrRef? defaultIface = type.GetDefaultInterface(); + + if (defaultIface is TypeDefinition di) + { + writer.Write("rc("); + TypedefNameWriter.WriteTypedefName(writer, context, type, TypedefNameType.NonProjected, true); + TypedefNameWriter.WriteTypeParams(writer, type); + writer.Write(";"); + WriteGuidSignature(writer, context, new TypeSemantics.Definition(di)); + writer.Write(")"); + } + else + { + writer.Write("{"); + WriteGuid(writer, type, true); + writer.Write("}"); + } + + break; + } + } + + /// + /// Writes a static IID property whose body is built from the parametric GUID signature. + /// + public static void WriteIidGuidPropertyFromSignature(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + string guidSig = WriteGuidSignature(context, new TypeSemantics.Definition(type)); + string ireferenceGuidSig = "pinterface({61c17706-2d65-11e0-9ae8-d48564015472};" + guidSig + ")"; + Guid guidValue = GuidGenerator.Generate(ireferenceGuidSig); + byte[] bytes = guidValue.ToByteArray(); + + writer.Write("public static ref readonly Guid "); + WriteIidReferenceGuidPropertyName(writer, context, type); + writer.WriteLine(); + writer.Write(""" + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ReadOnlySpan data = + [ + + """, isMultiline: true); + for (int i = 0; i < 16; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + writer.Write($"0x{bytes[i].ToString("X", CultureInfo.InvariantCulture)}"); + } + writer.WriteLine(); + writer.WriteLine(""" + ]; + return ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + } + } + """, isMultiline: true); + writer.WriteLine(); + } + + /// + /// Emits IID properties for any not-included interfaces transitively implemented by a class. + /// + public static void WriteIidGuidPropertyForClassInterfaces(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, System.Collections.Generic.HashSet interfacesEmitted) + { + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + // Resolve TypeRef -> TypeDefinition via metadata cache (so we pick up cross-module + // inherited interfaces, e.g. Windows.UI.Composition.IAnimationObject from a XAML class). + TypeDefinition? ifaceType = impl.Interface as TypeDefinition; + + if (ifaceType is null && impl.Interface is TypeReference tr) + { + (string trNs, string trNm) = tr.Names(); + ifaceType = ResolveCrossModuleType(context.Cache, trNs, trNm); + } + + if (ifaceType is null) + { + continue; + } + + (string ns, string nm) = ifaceType.Names(); + // Skip mapped types + if (MappedTypes.Get(ns, nm) is not null) + { + continue; + } + + // Skip generic interfaces + if (ifaceType.GenericParameters.Count != 0) + { + continue; + } + + // Skip already-emitted + if (interfacesEmitted.Contains(ifaceType)) + { + continue; + } + + // Only emit if the interface is not in the projection (otherwise it'll be emitted naturally) + if (!context.Settings.Filter.Includes(ifaceType)) + { + WriteIidGuidPropertyFromType(writer, context, ifaceType); + _ = interfacesEmitted.Add(ifaceType); + } + } + } + private static TypeDefinition? ResolveCrossModuleType(MetadataCache cache, string ns, string name) + { + return cache.Find(string.IsNullOrEmpty(ns) ? name : (ns + "." + name)); + } + + /// + /// Writes the InterfaceIIDs file header. + /// + public static void WriteInterfaceIidsBegin(IndentedTextWriter writer) + { + writer.WriteLine(); + writer.WriteLine($$""" + //------------------------------------------------------------------------------ + // + // This file was generated by cswinrt.exe version {{MetadataAttributeFactory.GetVersionString()}} + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + using System; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + namespace ABI; + + internal static class InterfaceIIDs + { + """, isMultiline: true); + } + + /// + /// Writes the InterfaceIIDs file footer. + /// + public static void WriteInterfaceIidsEnd(IndentedTextWriter writer) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + writer.WriteLine(); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.InteropTypeName.cs b/src/WinRT.Projection.Writer/Helpers/InteropTypeNameWriter.cs similarity index 59% rename from src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.InteropTypeName.cs rename to src/WinRT.Projection.Writer/Helpers/InteropTypeNameWriter.cs index d3235c81e8..8e9d4dceb4 100644 --- a/src/WinRT.Projection.Generator.Writer/Writers/CodeWriters.InteropTypeName.cs +++ b/src/WinRT.Projection.Writer/Helpers/InteropTypeNameWriter.cs @@ -2,19 +2,22 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Globalization; using System.Text; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Metadata; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; -namespace WindowsRuntime.ProjectionGenerator.Writer; +namespace WindowsRuntime.ProjectionWriter.Helpers; /// /// Encoder for the WinRT.Interop assembly type name format used in UnsafeAccessor /// attributes (e.g. "ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<string|object>Marshaller, WinRT.Interop"). -/// Mirrors the C++ helpers write_interop_assembly_name, write_interop_dll_type_name, -/// and write_interop_dll_type_name_for_typedef. /// -internal static partial class CodeWriters +internal static class InteropTypeNameWriter { /// /// Encodes a TypeSignature using the WinRT.Interop name format. Used as the value of an @@ -30,17 +33,22 @@ public static string EncodeInteropTypeName(TypeSignature sig, TypedefNameType na return sb.ToString(); } - private static void EncodeInteropTypeNameInto(StringBuilder sb, TypeSignature sig, TypedefNameType nameType) + /// + /// Encodes an ABI interop type name for into using the format expected by WindowsRuntime.InteropServices attributes. + /// + internal static void EncodeInteropTypeNameInto(StringBuilder sb, TypeSignature sig, TypedefNameType nameType) { - // Special case for System.Guid: matches C++ guid_type case in write_interop_dll_type_name. + // Special case for System.Guid: emitted with assembly-qualified form. if (sig is TypeDefOrRefSignature gtd && gtd.Type?.Namespace?.Value == "System" && gtd.Type?.Name?.Value == "Guid") { - if (nameType == TypedefNameType.Projected) { sb.Append("System-Guid"); } - else { sb.Append("ABI.System.<<#corlib>Guid>"); } + _ = nameType == TypedefNameType.Projected + ? sb.Append("System-Guid") + : sb.Append("ABI.System.<<#corlib>Guid>"); return; } + switch (sig) { case CorLibTypeSignature corlib: @@ -59,10 +67,11 @@ private static void EncodeInteropTypeNameInto(StringBuilder sb, TypeSignature si } else { - sb.Append("ABI.System.<"); + _ = sb.Append("ABI.System.<"); EncodeInteropTypeNameInto(sb, sz.BaseType, TypedefNameType.Projected); - sb.Append(">"); + _ = sb.Append(">"); } + return; case ByReferenceTypeSignature br: EncodeInteropTypeNameInto(sb, br.BaseType, nameType); @@ -71,142 +80,159 @@ private static void EncodeInteropTypeNameInto(StringBuilder sb, TypeSignature si EncodeInteropTypeNameInto(sb, cm.BaseType, nameType); return; default: - sb.Append(sig.FullName); + _ = sb.Append(sig.FullName); return; } } - private static void EncodeFundamental(StringBuilder sb, CorLibTypeSignature corlib, TypedefNameType nameType) + /// + /// Encodes the interop type name for a fundamental corlib type into . + /// + internal static void EncodeFundamental(StringBuilder sb, CorLibTypeSignature corlib, TypedefNameType nameType) { switch (corlib.ElementType) { - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Object: - if (nameType == TypedefNameType.Projected) { sb.Append("object"); } - else { sb.Append("ABI.System."); } + case ElementType.Object: + _ = nameType == TypedefNameType.Projected + ? sb.Append("object") + : sb.Append("ABI.System."); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Boolean: sb.Append("bool"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.Char: sb.Append("char"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I1: sb.Append("sbyte"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U1: sb.Append("byte"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I2: sb.Append("short"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U2: sb.Append("ushort"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I4: sb.Append("int"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U4: sb.Append("uint"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.I8: sb.Append("long"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.U8: sb.Append("ulong"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R4: sb.Append("float"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.R8: sb.Append("double"); return; - case AsmResolver.PE.DotNet.Metadata.Tables.ElementType.String: - sb.Append("string"); + case ElementType.Boolean: _ = sb.Append("bool"); return; + case ElementType.Char: _ = sb.Append("char"); return; + case ElementType.I1: _ = sb.Append("sbyte"); return; + case ElementType.U1: _ = sb.Append("byte"); return; + case ElementType.I2: _ = sb.Append("short"); return; + case ElementType.U2: _ = sb.Append("ushort"); return; + case ElementType.I4: _ = sb.Append("int"); return; + case ElementType.U4: _ = sb.Append("uint"); return; + case ElementType.I8: _ = sb.Append("long"); return; + case ElementType.U8: _ = sb.Append("ulong"); return; + case ElementType.R4: _ = sb.Append("float"); return; + case ElementType.R8: _ = sb.Append("double"); return; + case ElementType.String: + _ = sb.Append("string"); return; } - sb.Append(corlib.FullName); + _ = sb.Append(corlib.FullName); } - private static void EncodeForTypeDef(StringBuilder sb, ITypeDefOrRef type, TypedefNameType nameType, System.Collections.Generic.IList? generic_args) + private static void EncodeForTypeDef(StringBuilder sb, ITypeDefOrRef type, TypedefNameType nameType, IList? generic_args) { - string typeNs = type.Namespace?.Value ?? string.Empty; - string typeName = type.Name?.Value ?? string.Empty; + (string typeNs, string typeName) = type.Names(); + + bool isAbi = nameType is not (TypedefNameType.Projected or TypedefNameType.InteropIID); - bool isAbi = nameType != TypedefNameType.Projected && nameType != TypedefNameType.InteropIID; - if (isAbi) { sb.Append("ABI."); } + if (isAbi) + { + _ = sb.Append("ABI."); + } // Special case for EventSource on Windows.Foundation event-handler delegate types - // (e.g. EventHandler, TypedEventHandler). Mirrors C++: - // ABI.WindowsRuntime.InteropServices.<#CsWinRT>EventHandlerEventSource' - // Note the namespace check uses the ORIGINAL .winmd namespace (before mapping). - if (nameType == TypedefNameType.EventSource && typeNs == "Windows.Foundation") + // (e.g. EventHandler, TypedEventHandler). + if (nameType == TypedefNameType.EventSource && typeNs == WindowsFoundation) { // Determine generic arity from the .winmd type name (e.g. "EventHandler`1" => 1). int arity = 0; int tickIdx = typeName.IndexOf('`'); + if (tickIdx >= 0 && int.TryParse(typeName.AsSpan(tickIdx + 1), out int parsed)) { arity = parsed; } - sb.Append("WindowsRuntime.InteropServices.<#CsWinRT>EventHandlerEventSource'"); - sb.Append(arity.ToString(System.Globalization.CultureInfo.InvariantCulture)); + + _ = sb.Append("WindowsRuntime.InteropServices.<#CsWinRT>EventHandlerEventSource'"); + _ = sb.Append(arity.ToString(CultureInfo.InvariantCulture)); // Append the generic args (if any). if (generic_args is { Count: > 0 }) { - sb.Append('<'); + _ = sb.Append('<'); for (int i = 0; i < generic_args.Count; i++) { - if (i > 0) { sb.Append('|'); } + if (i > 0) + { + _ = sb.Append('|'); + } + EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); } - sb.Append('>'); + _ = sb.Append('>'); } + return; } // Apply mapped-type remapping MappedType? mapped = MappedTypes.Get(typeNs, typeName); - if (mapped is not null) + + if (mapped is { } m) { - typeNs = mapped.MappedNamespace; - typeName = mapped.MappedName; + typeNs = m.MappedNamespace; + typeName = m.MappedName; } + // Replace generic arity backtick with apostrophe. typeName = typeName.Replace('`', '\''); if (nameType == TypedefNameType.InteropIID) { - sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); - sb.Append(typeName); + _ = sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + _ = sb.Append(typeName); } else if (nameType == TypedefNameType.Projected) { // Replace namespace separator with - within the generic. string nsHyphenated = typeNs.Replace('.', '-'); - sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); - sb.Append(nsHyphenated); - sb.Append('-'); - sb.Append(typeName); + _ = sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + _ = sb.Append(nsHyphenated); + _ = sb.Append('-'); + _ = sb.Append(typeName); } else { - sb.Append(typeNs); - sb.Append('.'); - sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); - sb.Append(typeName); + _ = sb.Append(typeNs); + _ = sb.Append('.'); + _ = sb.Append(GetInteropAssemblyMarker(typeNs, typeName, mapped, type)); + _ = sb.Append(typeName); } if (generic_args is { Count: > 0 }) { - sb.Append('<'); + _ = sb.Append('<'); for (int i = 0; i < generic_args.Count; i++) { - if (i > 0) { sb.Append('|'); } + if (i > 0) + { + _ = sb.Append('|'); + } + EncodeInteropTypeNameInto(sb, generic_args[i], TypedefNameType.Projected); } - sb.Append('>'); + _ = sb.Append('>'); } - // Append the type-kind suffix (matches C++ write_interop_dll_type_name_for_typedef). + // Append the type-kind suffix (e.g. "Methods" for the static ABI methods class). if (nameType == TypedefNameType.StaticAbiClass) { - sb.Append("Methods"); + _ = sb.Append("Methods"); } else if (nameType == TypedefNameType.ABI) { - sb.Append("Marshaller"); + _ = sb.Append("Marshaller"); } else if (nameType == TypedefNameType.EventSource) { - sb.Append("EventSource"); + _ = sb.Append("EventSource"); } } /// /// Returns the assembly marker (e.g. <#corlib>) for a (possibly remapped) - /// type/namespace. Mirrors C++ write_interop_assembly_name. + /// type/namespace. /// - private static string GetInteropAssemblyMarker(string typeNs, string typeName, MappedType? mapped, ITypeDefOrRef? type = null) + internal static string GetInteropAssemblyMarker(string typeNs, string typeName, MappedType? mapped, ITypeDefOrRef? type = null) { - if (mapped is not null) + if (mapped is { } m) { - // Mirrors C++ helpers.h:693-725 + code_writers.h:441-466. The mapped namespace // determines the marker. if (typeNs.StartsWith("System", StringComparison.Ordinal)) { @@ -214,53 +240,62 @@ private static string GetInteropAssemblyMarker(string typeNs, string typeName, M { return ""; } + if (IsMappedTypeInSystemObjectModel(typeNs, typeName)) { return ""; } + return "<#corlib>"; } + // Mapped to a non-System namespace. - if (!mapped.EmitAbi) + if (!m.EmitAbi) { return "<#CsWinRT>"; } + if (typeNs.StartsWith("Windows", StringComparison.Ordinal)) { - // Mirror C++ code_writers.h:464 which writes "<#%Windows>" — the '%' is an - // unintended template placeholder in C++ that's unreachable in practice (no - // standard mapped type maps to a Windows.* namespace with EmitAbi=true). We - // emit the corrected '<#Windows>' so any future addition that hits this + // Unreachable in practice: no standard mapped type maps to a Windows.* namespace + // with EmitAbi=true. Emit '<#Windows>' so any future addition that hits this // branch produces a runtime-resolvable assembly marker rather than garbage. return "<#Windows>"; } } + // Unmapped type. if (typeNs.StartsWith("Windows.", StringComparison.Ordinal) || typeNs == "Windows") { return "<#Windows>"; } + if (typeNs.StartsWith("WindowsRuntime", StringComparison.Ordinal)) { return "<#CsWinRT>"; } + // For any other type (e.g. user-authored components in third-party .winmd assemblies), - // use the actual assembly name from the type's resolution scope. Mirrors C++ which + // use the actual assembly name from the type's resolution scope.. // uses the .winmd file stem (e.g. "AuthoringTest" for AuthoringTest.winmd). if (type is not null) { string? asmName = GetTypeAssemblyName(type); + if (!string.IsNullOrEmpty(asmName)) { - // Replace '.' with '-' (matches C++ which does std::replace('.', '-')). + // Replace '.' with '-' for the assembly tag (e.g. "WinRT.Interop" -> "WinRT-Interop"). string hyphenated = asmName.Replace('.', '-'); return "<" + hyphenated + ">"; } } + return "<#Windows>"; } - /// Mirrors C++ helpers.h:693 is_mapped_type_in_system_objectmodel. + /// + /// Returns whether the type lives in System.ObjectModel and is one of the recognized mapped types (used by interop type-name encoding). + /// private static bool IsMappedTypeInSystemObjectModel(string typeNs, string typeName) { if (typeNs == "System.Collections.Specialized") @@ -270,6 +305,7 @@ private static bool IsMappedTypeInSystemObjectModel(string typeNs, string typeNa or "NotifyCollectionChangedEventArgs" or "NotifyCollectionChangedEventHandler"; } + if (typeNs == "System.ComponentModel") { return typeName is "INotifyDataErrorInfo" @@ -278,14 +314,18 @@ private static bool IsMappedTypeInSystemObjectModel(string typeNs, string typeNa or "PropertyChangedEventArgs" or "PropertyChangedEventHandler"; } + if (typeNs == "System.Windows.Input") { return typeName == "ICommand"; } + return false; } - /// Mirrors C++ helpers.h:727 is_mapped_type_in_system_numerics_vectors. + /// + /// Returns whether the type lives in System.Numerics.Vectors and is one of the recognized mapped types (used by interop type-name encoding). + /// private static bool IsMappedTypeInSystemNumericsVectors(string typeNs) { return typeNs == "System.Numerics"; diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/MappedTypes.cs b/src/WinRT.Projection.Writer/Helpers/MappedTypes.cs similarity index 55% rename from src/WinRT.Projection.Generator.Writer/Helpers/MappedTypes.cs rename to src/WinRT.Projection.Writer/Helpers/MappedTypes.cs index 79dae1b2fd..a473fff02a 100644 --- a/src/WinRT.Projection.Generator.Writer/Helpers/MappedTypes.cs +++ b/src/WinRT.Projection.Writer/Helpers/MappedTypes.cs @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Frozen; using System.Collections.Generic; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; +using static WindowsRuntime.ProjectionWriter.References.WellKnownTypeNames; -namespace WindowsRuntime.ProjectionGenerator.Writer; +namespace WindowsRuntime.ProjectionWriter.Helpers; /// -/// Mirrors the C++ mapped_type struct in helpers.h. /// Maps a Windows Runtime type to the corresponding .NET type. /// -internal sealed record MappedType( +/// The Windows Runtime ABI type name. +/// The .NET namespace the WinRT type maps to. +/// The .NET type name the WinRT type maps to. +/// Whether values of the mapped type require marshalling at the projection boundary. +/// Whether the writer should emit custom member projections for the mapped type. +/// Whether the writer should emit an ABI projection for the mapped type. +internal readonly record struct MappedType( string AbiName, string MappedNamespace, string MappedName, @@ -18,36 +27,51 @@ internal sealed record MappedType( bool EmitAbi = false); /// -/// Static lookup table for Windows Runtime → .NET type mappings (from helpers.h). +/// Static lookup table for Windows Runtime → .NET type mappings. /// internal static class MappedTypes { - private static readonly Dictionary> _byNamespace = Build(); + private static readonly FrozenDictionary> TypeMappings = Build(); + /// + /// Returns the entry for the type identified by + /// (, ), or + /// if no mapping exists. + /// + /// The Windows Runtime namespace. + /// The Windows Runtime type name. + /// The mapping, or . public static MappedType? Get(string typeNamespace, string typeName) { - if (_byNamespace.TryGetValue(typeNamespace, out Dictionary? namesp) && - namesp.TryGetValue(typeName, out MappedType? mapped)) + if (TypeMappings.TryGetValue(typeNamespace, out FrozenDictionary? namesp) && + namesp.TryGetValue(typeName, out MappedType mapped)) { return mapped; } + return null; } - public static bool HasNamespace(string typeNamespace) => _byNamespace.ContainsKey(typeNamespace); + /// + /// Returns whether contains at least one mapped type. + /// + /// The Windows Runtime namespace. + /// if there is at least one mapping in this namespace. + public static bool HasNamespace(string typeNamespace) => TypeMappings.ContainsKey(typeNamespace); - private static Dictionary> Build() + private static FrozenDictionary> Build() { - Dictionary> result = new(System.StringComparer.Ordinal); + Dictionary> result = []; // 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); + bag = []; result[ns] = bag; } + bag[mt.AbiName] = mt; } @@ -115,59 +139,59 @@ void Add(string ns, MappedType mt) 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)); + Add(WindowsFoundation, new("AsyncActionCompletedHandler", WindowsFoundation, "AsyncActionCompletedHandler")); + Add(WindowsFoundation, new("AsyncActionProgressHandler`1", WindowsFoundation, "AsyncActionProgressHandler`1")); + Add(WindowsFoundation, new("AsyncActionWithProgressCompletedHandler`1", WindowsFoundation, "AsyncActionWithProgressCompletedHandler`1")); + Add(WindowsFoundation, new("AsyncOperationCompletedHandler`1", WindowsFoundation, "AsyncOperationCompletedHandler`1")); + Add(WindowsFoundation, new("AsyncOperationProgressHandler`2", WindowsFoundation, "AsyncOperationProgressHandler`2")); + Add(WindowsFoundation, new("AsyncOperationWithProgressCompletedHandler`2", WindowsFoundation, "AsyncOperationWithProgressCompletedHandler`2")); + Add(WindowsFoundation, new("AsyncStatus", WindowsFoundation, "AsyncStatus")); + Add(WindowsFoundation, new("DateTime", "System", "DateTimeOffset", true)); + Add(WindowsFoundation, new("EventHandler`1", "System", "EventHandler`1", false)); + Add(WindowsFoundation, new("EventRegistrationToken", "WindowsRuntime.InteropServices", "EventRegistrationToken", false)); + Add(WindowsFoundation, new("FoundationContract", WindowsFoundation, "FoundationContract")); + Add(WindowsFoundation, new(HResult, "System", "Exception", true)); + Add(WindowsFoundation, new("IAsyncAction", WindowsFoundation, "IAsyncAction")); + Add(WindowsFoundation, new("IAsyncActionWithProgress`1", WindowsFoundation, "IAsyncActionWithProgress`1")); + Add(WindowsFoundation, new("IAsyncInfo", WindowsFoundation, "IAsyncInfo")); + Add(WindowsFoundation, new("IAsyncOperationWithProgress`2", WindowsFoundation, "IAsyncOperationWithProgress`2")); + Add(WindowsFoundation, new("IAsyncOperation`1", WindowsFoundation, "IAsyncOperation`1")); + Add(WindowsFoundation, new("IClosable", "System", "IDisposable", true, true)); + Add(WindowsFoundation, new("IMemoryBufferReference", WindowsFoundation, "IMemoryBufferReference")); + Add(WindowsFoundation, new("IPropertyValue", WindowsFoundation, "IPropertyValue", true)); + Add(WindowsFoundation, new("IReferenceArray`1", WindowsFoundation, "IReferenceArray", true)); + Add(WindowsFoundation, new(IReferenceGeneric, "System", NullableGeneric, true)); + Add(WindowsFoundation, new("IStringable", WindowsFoundation, "IStringable")); + Add(WindowsFoundation, new("Point", WindowsFoundation, "Point")); + Add(WindowsFoundation, new("PropertyType", WindowsFoundation, "PropertyType")); + Add(WindowsFoundation, new("Rect", WindowsFoundation, "Rect")); + Add(WindowsFoundation, new("Size", WindowsFoundation, "Size")); + Add(WindowsFoundation, new("TimeSpan", "System", "TimeSpan", true)); + Add(WindowsFoundation, new("TypedEventHandler`2", "System", "EventHandler`2", false)); + Add(WindowsFoundation, new("UniversalApiContract", WindowsFoundation, "UniversalApiContract")); + Add(WindowsFoundation, 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")); + Add(WindowsFoundationCollections, new("CollectionChange", WindowsFoundationCollections, "CollectionChange")); + Add(WindowsFoundationCollections, new("IIterable`1", "System.Collections.Generic", "IEnumerable`1", true, true)); + Add(WindowsFoundationCollections, new("IIterator`1", "System.Collections.Generic", "IEnumerator`1", true, true)); + Add(WindowsFoundationCollections, new("IKeyValuePair`2", "System.Collections.Generic", "KeyValuePair`2", true)); + Add(WindowsFoundationCollections, new("IMapChangedEventArgs`1", WindowsFoundationCollections, "IMapChangedEventArgs`1")); + Add(WindowsFoundationCollections, new("IMapView`2", "System.Collections.Generic", "IReadOnlyDictionary`2", true, true)); + Add(WindowsFoundationCollections, new("IMap`2", "System.Collections.Generic", "IDictionary`2", true, true)); + Add(WindowsFoundationCollections, new("IObservableMap`2", WindowsFoundationCollections, "IObservableMap`2")); + Add(WindowsFoundationCollections, new("IObservableVector`1", WindowsFoundationCollections, "IObservableVector`1")); + Add(WindowsFoundationCollections, new("IVectorChangedEventArgs", WindowsFoundationCollections, "IVectorChangedEventArgs")); + Add(WindowsFoundationCollections, new("IVectorView`1", "System.Collections.Generic", "IReadOnlyList`1", true, true)); + Add(WindowsFoundationCollections, new("IVector`1", "System.Collections.Generic", "IList`1", true, true)); + Add(WindowsFoundationCollections, new("MapChangedEventHandler`2", WindowsFoundationCollections, "MapChangedEventHandler`2")); + Add(WindowsFoundationCollections, new("VectorChangedEventHandler`1", WindowsFoundationCollections, "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")); + Add(WindowsFoundationMetadata, new("ApiContractAttribute", WindowsFoundationMetadata, "ApiContractAttribute")); + Add(WindowsFoundationMetadata, new("AttributeTargets", "System", "AttributeTargets")); + Add(WindowsFoundationMetadata, new("AttributeUsageAttribute", "System", "AttributeUsageAttribute")); + Add(WindowsFoundationMetadata, new(ContractVersionAttribute, WindowsFoundationMetadata, ContractVersionAttribute)); // Windows.Foundation.Numerics Add("Windows.Foundation.Numerics", new("Matrix3x2", "System.Numerics", "Matrix3x2")); @@ -219,15 +243,15 @@ void Add(string ns, MappedType mt) 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)); + Add(WindowsUIXamlInterop, new("IBindableIterable", "System.Collections", "IEnumerable", true, true)); + Add(WindowsUIXamlInterop, new("IBindableIterator", "System.Collections", "IEnumerator", true, true)); + Add(WindowsUIXamlInterop, new("IBindableVector", "System.Collections", "IList", true, true)); + Add(WindowsUIXamlInterop, new("INotifyCollectionChanged", "System.Collections.Specialized", "INotifyCollectionChanged", true)); + Add(WindowsUIXamlInterop, new("NotifyCollectionChangedAction", "System.Collections.Specialized", "NotifyCollectionChangedAction")); + Add(WindowsUIXamlInterop, new("NotifyCollectionChangedEventArgs", "System.Collections.Specialized", "NotifyCollectionChangedEventArgs", true)); + Add(WindowsUIXamlInterop, new("NotifyCollectionChangedEventHandler", "System.Collections.Specialized", "NotifyCollectionChangedEventHandler", true)); + Add(WindowsUIXamlInterop, new("TypeKind", WindowsUIXamlInterop, "TypeKind", true)); + Add(WindowsUIXamlInterop, new(TypeName, "System", "Type", true)); // Windows.UI.Xaml.Media Add("Windows.UI.Xaml.Media", new("IMatrixHelper", "", "")); @@ -251,9 +275,14 @@ void Add(string ns, MappedType mt) Add("Windows.UI.Xaml.Media.Media3D", new("Matrix3DHelper", "", "")); // WindowsRuntime.Internal - Add("WindowsRuntime.Internal", new("HWND", "System", "IntPtr")); - Add("WindowsRuntime.Internal", new("ProjectionInternalAttribute", "", "")); + Add(WindowsRuntimeInternal, new("HWND", "System", "IntPtr")); + Add(WindowsRuntimeInternal, new("ProjectionInternalAttribute", "", "")); - return result; + Dictionary> frozenInner = []; + foreach (KeyValuePair> kvp in result) + { + frozenInner[kvp.Key] = kvp.Value.ToFrozenDictionary(); + } + return frozenInner.ToFrozenDictionary(); } -} +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/ObjRefNameGenerator.cs b/src/WinRT.Projection.Writer/Helpers/ObjRefNameGenerator.cs new file mode 100644 index 0000000000..769231693f --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/ObjRefNameGenerator.cs @@ -0,0 +1,553 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Factories; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// ObjRef field emission for runtime classes. +/// +internal static class ObjRefNameGenerator +{ + /// + /// Returns the field name for the given interface impl (e.g. _objRef_System_IDisposable). + /// Strips the global:: prefix and replaces non-identifier characters with _. + /// + public static string GetObjRefName(ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + // Build the projected, fully-qualified name with global::. + string projected; + + if (ifaceType is TypeDefinition td) + { + (string ns, string name) = td.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + projected = GlobalPrefix + ns + "." + IdentifierEscaping.StripBackticks(name); + } + else if (ifaceType is TypeReference tr) + { + (string ns, string name) = tr.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + projected = GlobalPrefix + ns + "." + IdentifierEscaping.StripBackticks(name); + } + else + { + // Generic instantiation: always use fully qualified name (with global::) for the objref + // name computation, so the resulting field name is unique across namespaces. + projected = WriteFullyQualifiedInterfaceName(context, ifaceType); + } + + return "_objRef_" + IidExpressionGenerator.EscapeTypeNameForIdentifier(projected, stripGlobal: true); + } + + /// + /// Like + /// but always emits a fully qualified name with global:: prefix on every type + /// (even same-namespace ones). Used for objref name computation where uniqueness across + /// namespaces matters. + /// + private static void WriteFullyQualifiedInterfaceName(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + if (ifaceType is TypeDefinition td) + { + (string ns, string name) = td.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + writer.Write(GlobalPrefix); + + if (!string.IsNullOrEmpty(ns)) + { + writer.Write($"{ns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(name)); + } + else if (ifaceType is TypeReference tr) + { + (string ns, string name) = tr.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + writer.Write(GlobalPrefix); + + if (!string.IsNullOrEmpty(ns)) + { + writer.Write($"{ns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(name)); + } + else if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ITypeDefOrRef gt = gi.GenericType; + (string ns, string name) = gt.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + writer.Write(GlobalPrefix); + + if (!string.IsNullOrEmpty(ns)) + { + writer.Write($"{ns}."); + } + + writer.Write($"{IdentifierEscaping.StripBackticks(name)}<"); + for (int i = 0; i < gi.TypeArguments.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + // forceWriteNamespace=true so generic args also get global:: prefix. + TypedefNameWriter.WriteTypeName(writer, context, TypeSemanticsFactory.Get(gi.TypeArguments[i]), TypedefNameType.Projected, true); + } + + writer.Write(">"); + } + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the fully-qualified interface name into it, and returns the resulting string. + /// + /// The active emit context. + /// The interface type whose fully-qualified name is emitted. + /// The emitted fully-qualified interface name. + private static string WriteFullyQualifiedInterfaceName(ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteFullyQualifiedInterfaceName(writer, context, ifaceType); + return writer.ToString(); + } + + /// + /// Writes the IID expression for the given interface impl (used as the second arg to + /// NativeObjectReference.As(...)). + /// + public static void WriteIidExpression(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + // Generic instantiation: use the UnsafeAccessor extern method declared above the field. + if (ifaceType is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + string propName = BuildIidPropertyNameForGenericInterface(context, gi); + writer.Write($"{propName}(null)"); + return; + } + + string ns; + string name; + bool isMapped; + + if (ifaceType is TypeDefinition td) + { + ns = td.Namespace?.Value ?? string.Empty; + name = td.Name?.Value ?? string.Empty; + isMapped = MappedTypes.Get(ns, name) is not null; + } + else if (ifaceType is TypeReference tr) + { + ns = tr.Namespace?.Value ?? string.Empty; + name = tr.Name?.Value ?? string.Empty; + isMapped = MappedTypes.Get(ns, name) is not null; + } + else + { + writer.Write("default(global::System.Guid)"); + return; + } + + if (isMapped) + { + // IStringable maps to a simpler IID name in WellKnownInterfaceIIDs. + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { MappedName: "IStringable" }) + { + writer.Write("global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable"); + return; + } + + // Mapped interface: use WellKnownInterfaceIIDs.IID_. + string id = EscapeIdentifier(ns + "." + IdentifierEscaping.StripBackticks(name)); + writer.Write($"global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_{id}"); + } + else + { + // Non-mapped, non-generic: ABI.InterfaceIIDs.IID_. + string abiQualified = GlobalAbiPrefix + ns + "." + IdentifierEscaping.StripBackticks(name); + string id = IidExpressionGenerator.EscapeTypeNameForIdentifier(abiQualified, stripGlobal: false, stripGlobalABI: true); + writer.Write($"global::ABI.InterfaceIIDs.IID_{id}"); + } + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the IID expression into it, and returns the resulting string. + /// + /// The active emit context. + /// The interface type whose IID expression is emitted. + /// The emitted IID expression. + public static string WriteIidExpression(ProjectionEmitContext context, ITypeDefOrRef ifaceType) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteIidExpression(writer, context, ifaceType); + return writer.ToString(); + } + + /// + /// Builds the IID property name for a generic interface instantiation. + /// E.g. IObservableMap<string, object> -> IID_Windows_Foundation_Collections_IObservableMap_string__object_. + /// + internal static string BuildIidPropertyNameForGenericInterface(ProjectionEmitContext context, GenericInstanceTypeSignature gi) + { + TypeSemantics sem = TypeSemanticsFactory.Get(gi); + return "IID_" + IidExpressionGenerator.EscapeTypeNameForIdentifier( + TypedefNameWriter.WriteTypeName(context, sem, TypedefNameType.ABI, forceWriteNamespace: true), + stripGlobal: true, stripGlobalABI: true); + } + + /// + /// Emits the [UnsafeAccessor] extern method declaration that exposes the IID for a generic + /// interface instantiation. + /// + /// The writer to emit to. + /// The active emit context. + /// The generic interface instantiation whose IID accessor is being emitted. + /// When true, the accessor's parameter type is + /// object? (used inside #nullable enable regions); otherwise object. + internal static void EmitUnsafeAccessorForIid(IndentedTextWriter writer, ProjectionEmitContext context, GenericInstanceTypeSignature gi, bool isInNullableContext = false) + { + string propName = BuildIidPropertyNameForGenericInterface(context, gi); + string interopName = InteropTypeNameWriter.EncodeInteropTypeName(gi, TypedefNameType.InteropIID); + writer.Write($$""" + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_IID_{{interopName}}")] + static extern ref readonly Guid {{propName}}([UnsafeAccessorType("ABI.InterfaceIIDs, WinRT.Interop")] object + """, isMultiline: true); + if (isInNullableContext) + { + writer.Write("?"); + } + + writer.WriteLine(" _);"); + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the [UnsafeAccessor] declaration into it, and returns the resulting string. + /// + /// The active emit context. + /// The generic interface instantiation whose IID accessor is being emitted. + /// When true, the accessor's parameter type is object?; otherwise object. + /// The emitted [UnsafeAccessor] declaration. + internal static string EmitUnsafeAccessorForIid(ProjectionEmitContext context, GenericInstanceTypeSignature gi, bool isInNullableContext = false) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + EmitUnsafeAccessorForIid(writer, context, gi, isInNullableContext); + return writer.ToString(); + } + private static string EscapeIdentifier(string s) + { + System.Text.StringBuilder sb = new(s.Length); + foreach (char c in s) + { + _ = sb.Append(c is ' ' or ':' or '<' or '>' or '`' or ',' or '.' ? '_' : c); + } + return sb.ToString(); + } + + /// + /// Writes the IReference<T> IID expression for a value type (used by BoxToUnmanaged). + /// + public static void WriteIidReferenceExpression(IndentedTextWriter writer, TypeDefinition type) + { + (string ns, string name) = type.Names(); + string abiQualified = GlobalAbiPrefix + ns + "." + IdentifierEscaping.StripBackticks(name); + string id = IidExpressionGenerator.EscapeTypeNameForIdentifier(abiQualified, stripGlobal: false, stripGlobalABI: true); + writer.Write($"global::ABI.InterfaceIIDs.IID_{id}Reference"); + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the IID reference expression into it, and returns the resulting string. + /// + /// The value type whose IReference<T> IID expression is emitted. + /// The emitted IID reference expression. + public static string WriteIidReferenceExpression(TypeDefinition type) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteIidReferenceExpression(writer, type); + return writer.ToString(); + } + + /// + /// Emits the lazy _objRef_* field definitions for each interface implementation on + /// the given runtime class. For sealed classes, the default interface is emitted as a + /// simple => NativeObjectReference expression; for unsealed classes, ALL interfaces + /// (including the default) use the lazy MakeObjectReference pattern, and the default also + /// gets an init; accessor so the constructor can set it via + /// _objRef_X = NativeObjectReference. + /// + public static void WriteClassObjRefDefinitions(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + { + // Per-interface _objRef_* getters are emitted in BOTH impl and ref modes with full + // bodies. (Only the static factory _objRef_* getters become `throw null;` in ref mode -- + // see WriteStaticFactoryObjRef and WriteAttributedTypes.) + + // Track names emitted so we don't emit duplicates (e.g. when both IFoo and IFoo2 + // produce the same _objRef_). + HashSet emitted = []; + bool isSealed = type.IsSealed; + + // Pass 1: emit objrefs for ALL directly-declared interfaces first (in InterfaceImpl + // declaration order). Pass 2 then walks transitive parents to cover any not yet emitted. + // So for a class that declares 'IFoo, IBar' where IFoo : IBaz, the order is IFoo, IBar, + // IBaz, NOT IFoo, IBaz, IBar. + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + if (!ClassMembersFactory.IsInterfaceInInheritanceList(context.Cache, impl, includeExclusiveInterface: false) + && !IsInterfaceForObjRef(impl)) + { + continue; + } + + // For FastAbi classes, skip non-default exclusive interfaces -- their methods + // dispatch through the default interface's vtable so a separate objref is unnecessary. + bool isDefault = impl.HasAttribute(WindowsFoundationMetadata, DefaultAttribute); + + if (!isDefault && ClassFactory.IsFastAbiClass(type)) + { + TypeDefinition? implTypeDef = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, impl.Interface); + + if (implTypeDef is not null && TypeCategorization.IsExclusiveTo(implTypeDef)) + { + continue; + } + } + + EmitObjRefForInterface(writer, context, impl.Interface, emitted, isDefault: isDefault, useSimplePattern: isSealed && isDefault); + } + foreach (InterfaceImplementation impl in type.Interfaces) + { + if (impl.Interface is null) + { + continue; + } + + if (!ClassMembersFactory.IsInterfaceInInheritanceList(context.Cache, impl, includeExclusiveInterface: false) + && !IsInterfaceForObjRef(impl)) + { + continue; + } + + // Same fast-abi guard as the first pass. + bool isDefault2 = impl.HasAttribute(WindowsFoundationMetadata, DefaultAttribute); + + if (!isDefault2 && ClassFactory.IsFastAbiClass(type)) + { + TypeDefinition? implTypeDef = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, impl.Interface); + + if (implTypeDef is not null && TypeCategorization.IsExclusiveTo(implTypeDef)) + { + continue; + } + } + + EmitTransitiveInterfaceObjRefs(writer, context, impl.Interface, emitted); + } + } + + /// + /// Emits an _objRef_ field for a single interface impl reference. + /// + /// The writer to emit to. + /// The active emit context. + /// The interface reference being objref-d. + /// A set tracking objref names already emitted into the current class (deduplication). + /// When true, the interface is the runtime class's default interface (which the WindowsRuntimeObject base already exposes). + /// When true, emit the simple expression-bodied form + /// => NativeObjectReference. Otherwise emit the lazy MakeObjectReference pattern. + private static void EmitObjRefForInterface(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef ifaceRef, HashSet emitted, bool isDefault, bool useSimplePattern = false) + { + string objRefName = GetObjRefName(context, ifaceRef); + + if (!emitted.Add(objRefName)) + { + return; + } + + // The [UnsafeAccessor] extern method declaration is used by the IID expression in both + // simple and lazy patterns. + bool isGenericInstance = ifaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature; + GenericInstanceTypeSignature? gi = isGenericInstance + ? (GenericInstanceTypeSignature)((TypeSpecification)ifaceRef).Signature! + : null; + + if (useSimplePattern) + { + // Sealed-class default interface: simple expression-bodied property pointing at NativeObjectReference. + writer.WriteLine($"private WindowsRuntimeObjectReference {objRefName} => NativeObjectReference;"); + // Emit the unsafe accessor AFTER the field so it can be used to pass the IID in the + // constructor for the default interface. + if (gi is not null) + { + EmitUnsafeAccessorForIid(writer, context, gi); + } + } + else + { + // Emit the unsafe accessor BEFORE the lazy field so it's referenced inside the As(...) call. + if (gi is not null) + { + EmitUnsafeAccessorForIid(writer, context, gi); + } + + // Lazy CompareExchange pattern. For unsealed-class defaults, also emit 'init;' so the + // constructor can assign NativeObjectReference for the exact-type case. + writer.Write($$""" + private WindowsRuntimeObjectReference {{objRefName}} + { + get + { + [MethodImpl(MethodImplOptions.NoInlining)] + WindowsRuntimeObjectReference MakeObjectReference() + { + _ = global::System.Threading.Interlocked.CompareExchange( + location1: ref field, + value: NativeObjectReference.As( + """, isMultiline: true); + WriteIidExpression(writer, context, ifaceRef); + writer.WriteLine(""" + ), + comparand: null); + + return field; + } + + return field ?? MakeObjectReference(); + } + """, isMultiline: true); + if (isDefault) + { + writer.WriteLine(" init;"); + } + + writer.WriteLine("}"); + } + } + + /// + /// Walks transitively-inherited interfaces and emits an objref field for each one. + /// + private static void EmitTransitiveInterfaceObjRefs(IndentedTextWriter writer, ProjectionEmitContext context, ITypeDefOrRef ifaceRef, HashSet emitted) + { + // Resolve the interface to its TypeDefinition; if cross-module, look it up in the cache. + TypeDefinition? ifaceTd = AbiTypeHelpers.ResolveInterfaceTypeDef(context.Cache, ifaceRef); + + if (ifaceTd is null) + { + return; + } + + // Compute a substitution context if the parent is a closed generic instance. + GenericContext? ctx = null; + + if (ifaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gi) + { + ctx = new GenericContext(gi, null); + } + + foreach (InterfaceImplementation childImpl in ifaceTd.Interfaces) + { + if (childImpl.Interface is null) + { + continue; + } + + // If the parent is a closed generic, substitute the child's signature. + ITypeDefOrRef childRef = childImpl.Interface; + + if (ctx is not null) + { + TypeSignature childSig = childRef.ToTypeSignature(false); + TypeSignature substitutedSig = childSig.InstantiateGenericTypes(ctx.Value); + ITypeDefOrRef? newRef = substitutedSig.ToTypeDefOrRef(); + + if (newRef is not null) + { + childRef = newRef; + } + } + + // Emitting an _objRef_* field for an exclusive-to-someone-else parent interface is + // harmless (the field stores the result of a QI; if QI fails the field stays unset). + EmitObjRefForInterface(writer, context, childRef, emitted, isDefault: false); + EmitTransitiveInterfaceObjRefs(writer, context, childRef, emitted); + } + } + + /// + /// Whether this interface impl needs an _objRef_* field even though it isn't part of the + /// inheritance list (e.g. ExclusiveTo interfaces still need their objref since instance + /// methods/properties dispatch through it). + /// + public static bool IsInterfaceForObjRef(InterfaceImplementation impl) + { + // Emit objrefs for ALL implemented interfaces -- instance member dispatch needs to be + // able to reach them. + return impl.Interface is not null; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs b/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs new file mode 100644 index 0000000000..1c2d64215c --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/TypeFilter.cs @@ -0,0 +1,176 @@ +// 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.ProjectionWriter.Helpers; + +/// +/// Include/exclude type filter using longest-prefix-match semantics: type/namespace is checked +/// against each prefix in the include/exclude lists, and the longest matching prefix wins. +/// +internal sealed class TypeFilter +{ + private readonly List _include; + private readonly List _exclude; + + /// + /// Gets a default that has no include and no exclude rules + /// (and therefore matches every type). + /// + public static TypeFilter Empty { get; } = new([], []); + + /// + /// Initializes a new with the given include and exclude prefix lists. + /// + /// The include prefixes (a type matches if any prefix matches; empty means match-all). + /// The exclude prefixes (a type is rejected if any prefix matches and no include prefix wins). + public TypeFilter(IEnumerable include, IEnumerable exclude) + { + _include = [.. include.OrderByDescending(s => s.Length)]; + _exclude = [.. exclude.OrderByDescending(s => s.Length)]; + } + + /// + /// Whether this filter matches everything by default (no include rules). + /// + public bool MatchesAllByDefault => _include.Count == 0; + + /// + /// Returns whether the given type name passes the include/exclude filter. + /// 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.Count == 0 && _exclude.Count == 0) + { + return true; + } + + // Split into namespace + typename at the LAST '.'. + int dot = fullName.LastIndexOf('.'); + string ns; + string name; + + if (dot < 0) + { + ns = fullName; + name = fullName; + } + else + { + ns = fullName[..dot]; + name = fullName[(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 = incIdx < _include.Count ? _include[incIdx] : null; + string? excRule = 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 (this is the documented tie-breaker). + 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.Count == 0; + } + + 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[(typeNamespace.Length + 1)..]; + return typeName.StartsWith(rest, StringComparison.Ordinal); + } + + /// + /// Returns whether the given passes the include/exclude filter. + /// Computes the type's full name (namespace.typeName) and delegates to + /// . + /// + /// The type definition to test. + /// if the type's full name is included. + public bool Includes(TypeDefinition type) + { + return Includes(GetFullName(type)); + } + + /// + /// Returns the full name of in the namespace.typeName form + /// (or just the type name when the namespace is empty). + /// + /// The type definition to format. + /// The fully-qualified type name. + 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); + } +} \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs b/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs new file mode 100644 index 0000000000..5128804681 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs @@ -0,0 +1,441 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Writers; +using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Type-name emission helpers. +/// +internal static class TypedefNameWriter +{ + /// + /// Writes a fundamental (primitive) type's projected name. + /// + /// The writer to emit to. + /// The fundamental type. + public static void WriteFundamentalType(IndentedTextWriter writer, FundamentalType t) + { + writer.Write(FundamentalTypes.ToCSharpType(t)); + } + + /// + /// Writes a fundamental (primitive) type's non-projected (.NET BCL) name. + /// + /// The writer to emit to. + /// The fundamental type. + public static void WriteFundamentalNonProjectedType(IndentedTextWriter writer, FundamentalType t) + { + writer.Write(FundamentalTypes.ToDotNetType(t)); + } + + /// + /// Writes the C# type name for a typed reference. + /// + /// The writer to emit to. + /// The active emit context (provides settings, current namespace, ABI/ABI.Impl mode flags). + /// The type definition to emit the name of. + /// The kind of name to emit (projected, non-projected, ABI, etc.). + /// When , always prepend the global::-qualified namespace prefix. + public static void WriteTypedefName(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + bool authoredType = context.Settings.Component && context.Settings.Filter.Includes(type); + (string typeNamespace, string typeName) = type.Names(); + + if (nameType == TypedefNameType.NonProjected) + { + writer.Write($"{typeNamespace}.{typeName}"); + return; + } + + MappedType? proj = MappedTypes.Get(typeNamespace, typeName); + + if (proj is { } p) + { + typeNamespace = p.MappedNamespace; + typeName = p.MappedName; + } + + TypedefNameType nameToWrite = nameType; + + if (authoredType && TypeCategorization.IsExclusiveTo(type) && nameToWrite == TypedefNameType.Projected) + { + nameToWrite = TypedefNameType.CCW; + } + + // Authored interfaces that aren't exclusive use the same authored interface. + if (authoredType && nameToWrite == TypedefNameType.CCW && + TypeCategorization.GetCategory(type) == TypeCategory.Interface && + !TypeCategorization.IsExclusiveTo(type)) + { + nameToWrite = TypedefNameType.Projected; + } + + if (nameToWrite == TypedefNameType.EventSource && typeNamespace == "System") + { + writer.Write("global::WindowsRuntime.InteropServices."); + } + else if (forceWriteNamespace || + typeNamespace != context.CurrentNamespace || + (nameToWrite == TypedefNameType.Projected && (context.InAbiNamespace || context.InAbiImplNamespace)) || + (nameToWrite == TypedefNameType.ABI && !context.InAbiNamespace) || + (nameToWrite == TypedefNameType.EventSource && !context.InAbiNamespace) || + (nameToWrite == TypedefNameType.CCW && authoredType && !context.InAbiImplNamespace) || + (nameToWrite == TypedefNameType.CCW && !authoredType && (context.InAbiNamespace || context.InAbiImplNamespace))) + { + writer.Write(GlobalPrefix); + + if (nameToWrite is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) + { + writer.Write("ABI."); + } + else if (authoredType && nameToWrite == TypedefNameType.CCW) + { + writer.Write("ABI.Impl."); + } + + writer.Write($"{typeNamespace}."); + } + + if (nameToWrite == TypedefNameType.StaticAbiClass) + { + writer.Write($"{IdentifierEscaping.StripBackticks(typeName)}Methods"); + } + else if (nameToWrite == TypedefNameType.EventSource) + { + writer.Write($"{IdentifierEscaping.StripBackticks(typeName)}EventSource"); + } + else + { + writer.Write(IdentifierEscaping.StripBackticks(typeName)); + } + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the typedef name into it, and returns the resulting string. + /// + /// The active emit context. + /// The type definition to emit the name of. + /// The kind of name to emit (projected, non-projected, ABI, etc.). + /// When , always prepend the global::-qualified namespace prefix. + /// The emitted typedef name. + public static string WriteTypedefName(ProjectionEmitContext context, TypeDefinition type, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteTypedefName(writer, context, type, nameType, forceWriteNamespace); + return writer.ToString(); + } + + /// + /// Convenience helper that emits the typedef name immediately followed by the generic + /// parameter list (e.g. Foo<T0, T1>) into a pooled writer and returns the + /// resulting string. Equivalent to calling + /// + /// followed by . + /// + /// The active emit context. + /// The (potentially generic) type definition. + /// The kind of name to emit. + /// When , always prepend the global::-qualified namespace prefix. + /// The emitted typedef name + generic-parameter list. + public static string WriteTypedefNameWithTypeParams(ProjectionEmitContext context, TypeDefinition type, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteTypedefName(writer, context, type, nameType, forceWriteNamespace); + WriteTypeParams(writer, type); + return writer.ToString(); + } + + /// + /// Writes <T1, T2> for generic types. + /// + /// The writer to emit to. + /// The (potentially generic) type definition. + public static void WriteTypeParams(IndentedTextWriter writer, TypeDefinition type) + { + if (type.GenericParameters.Count == 0) + { + return; + } + + writer.Write("<"); + for (int i = 0; i < type.GenericParameters.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + string? gpName = type.GenericParameters[i].Name?.Value; + writer.Write(gpName ?? $"T{i}"); + } + writer.Write(">"); + } + + /// + /// Writes the typedef name + generic params for a handle. + /// + /// The writer to emit to. + /// The active emit context. + /// The semantic representation of the type. + /// The kind of name to emit. + /// When , always prepend the global::-qualified namespace prefix. + public static void WriteTypeName(IndentedTextWriter writer, ProjectionEmitContext context, TypeSemantics semantics, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + switch (semantics) + { + case TypeSemantics.Fundamental f: + WriteFundamentalType(writer, f.Type); + break; + case TypeSemantics.ObjectType: + writer.Write("object"); + break; + case TypeSemantics.GuidType: + writer.Write("Guid"); + break; + case TypeSemantics.SystemType: + writer.Write("Type"); + break; + case TypeSemantics.Definition d: + WriteTypedefName(writer, context, d.Type, nameType, forceWriteNamespace); + WriteTypeParams(writer, d.Type); + break; + case TypeSemantics.GenericInstance gi: + WriteTypedefName(writer, context, gi.GenericType, nameType, forceWriteNamespace); + writer.Write("<"); + for (int i = 0; i < gi.GenericArgs.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + // Generic args ALWAYS use Projected, regardless of parent's nameType. + WriteTypeName(writer, context, gi.GenericArgs[i], TypedefNameType.Projected, forceWriteNamespace); + } + writer.Write(">"); + break; + case TypeSemantics.GenericInstanceRef gir: + { + (string ns, string name) = gir.GenericType.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + if (nameType == TypedefNameType.EventSource && ns == "System") + { + writer.Write("global::WindowsRuntime.InteropServices."); + } + else if (!string.IsNullOrEmpty(ns)) + { + writer.Write(GlobalPrefix); + + if (nameType is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) + { + writer.Write("ABI."); + } + + writer.Write($"{ns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(name)); + + if (nameType == TypedefNameType.StaticAbiClass) + { + writer.Write("Methods"); + } + else if (nameType == TypedefNameType.EventSource) + { + writer.Write("EventSource"); + } + + writer.Write("<"); + for (int i = 0; i < gir.GenericArgs.Count; i++) + { + if (i > 0) + { + writer.Write(", "); + } + + WriteTypeName(writer, context, gir.GenericArgs[i], TypedefNameType.Projected, forceWriteNamespace); + } + writer.Write(">"); + } + break; + case TypeSemantics.Reference r: + { + (string ns, string name) = r.Type.Names(); + MappedType? mapped = MappedTypes.Get(ns, name); + + if (mapped is { } m) + { + ns = m.MappedNamespace; + name = m.MappedName; + } + + bool needsNsPrefix = !string.IsNullOrEmpty(ns) && ( + forceWriteNamespace || + ns != context.CurrentNamespace || + (nameType == TypedefNameType.Projected && (context.InAbiNamespace || context.InAbiImplNamespace)) || + nameType == TypedefNameType.ABI || + nameType == TypedefNameType.EventSource); + + if (needsNsPrefix) + { + writer.Write(GlobalPrefix); + + if (nameType is TypedefNameType.ABI or TypedefNameType.StaticAbiClass or TypedefNameType.EventSource) + { + writer.Write("ABI."); + } + + writer.Write($"{ns}."); + } + + writer.Write(IdentifierEscaping.StripBackticks(name)); + + if (nameType == TypedefNameType.StaticAbiClass) + { + writer.Write("Methods"); + } + else if (nameType == TypedefNameType.EventSource) + { + writer.Write("EventSource"); + } + } + break; + case TypeSemantics.GenericTypeIndex gti: + writer.Write($"T{gti.Index}"); + break; + } + } + + /// + /// Writes a projected type name (.NET-style). + /// + /// The writer to emit to. + /// The active emit context. + /// The semantic representation of the type. + public static void WriteProjectionType(IndentedTextWriter writer, ProjectionEmitContext context, TypeSemantics semantics) + { + WriteTypeName(writer, context, semantics, TypedefNameType.Projected, false); + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the type name into it, and returns the resulting string. + /// + /// The active emit context. + /// The semantic representation of the type. + /// The kind of name to emit. + /// When , always prepend the global::-qualified namespace prefix. + /// The emitted type name. + public static string WriteTypeName(ProjectionEmitContext context, TypeSemantics semantics, TypedefNameType nameType = TypedefNameType.Projected, bool forceWriteNamespace = false) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteTypeName(writer, context, semantics, nameType, forceWriteNamespace); + return writer.ToString(); + } + + /// + /// Convenience overload of + /// that leases an from , + /// emits the projected type name into it, and returns the resulting string. + /// + /// The active emit context. + /// The semantic representation of the type. + /// The emitted projected type name. + public static string WriteProjectionType(ProjectionEmitContext context, TypeSemantics semantics) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteProjectionType(writer, context, semantics); + return writer.ToString(); + } + + /// + /// Writes the event handler type for an EventDefinition. Handles all the cases: + /// TypeDefinition, TypeReference, TypeSpecification (generic instances like EventHandler<T>), + /// and any other ITypeDefOrRef. + /// + public static void WriteEventType(IndentedTextWriter writer, ProjectionEmitContext context, EventDefinition evt) + => WriteEventType(writer, context, evt, null); + + /// + /// Convenience overload of + /// that leases an from , + /// emits the event handler type into it, and returns the resulting string. + /// + /// The active emit context. + /// The event definition whose handler type is emitted. + /// The emitted event handler type name. + public static string WriteEventType(ProjectionEmitContext context, EventDefinition evt) + { + using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); + IndentedTextWriter writer = writerOwner.Writer; + WriteEventType(writer, context, evt, null); + return writer.ToString(); + } + + /// + /// Same as + /// but applies the supplied generic context for substitution (e.g., T0/T1 -> + /// concrete type arguments when emitting members for an instantiated parent generic interface). + /// + public static void WriteEventType(IndentedTextWriter writer, ProjectionEmitContext context, EventDefinition evt, GenericInstanceTypeSignature? currentInstance) + { + if (evt.EventType is null) + { + writer.Write("global::Windows.Foundation.EventHandler"); + return; + } + + TypeSignature sig = evt.EventType.ToTypeSignature(false); + + if (currentInstance is not null) + { + sig = sig.InstantiateGenericTypes(new GenericContext(currentInstance, null)); + } + + // Special case for Microsoft.UI.Xaml.Input.ICommand.CanExecuteChanged: the WinRT event + // handler is EventHandler but C# expects non-generic EventHandler. + if (evt.Name?.Value == "CanExecuteChanged" + && evt.DeclaringType is { } declaringType + && (declaringType.FullName is "Microsoft.UI.Xaml.Input.ICommand" or "Windows.UI.Xaml.Input.ICommand")) + { + // Verify the event type matches EventHandler before applying override. + if (sig is GenericInstanceTypeSignature gi + && gi.GenericType.Namespace?.Value == WindowsFoundation + && gi.GenericType.Name?.Value == "EventHandler`1" + && gi.TypeArguments.Count == 1 + && gi.TypeArguments[0] is CorLibTypeSignature corlib + && corlib.ElementType == ElementType.Object) + { + writer.Write("global::System.EventHandler"); + return; + } + } + + // The outer EventHandler still gets 'global::System.' from being in a different namespace, + // but type args in the same namespace stay unqualified. + WriteTypeName(writer, context, TypeSemanticsFactory.Get(sig), TypedefNameType.Projected, false); + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/WellKnownInterfaceEntriesEmitter.cs b/src/WinRT.Projection.Writer/Helpers/WellKnownInterfaceEntriesEmitter.cs new file mode 100644 index 0000000000..cd3f469025 --- /dev/null +++ b/src/WinRT.Projection.Writer/Helpers/WellKnownInterfaceEntriesEmitter.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using WindowsRuntime.ProjectionWriter.Writers; + +namespace WindowsRuntime.ProjectionWriter.Helpers; + +/// +/// Emits the IID/Vtable initializer pairs for the well-known WinRT interfaces (IPropertyValue, +/// IStringable, IWeakReferenceSource, IMarshal, IAgileObject, IInspectable, IUnknown) that +/// every CCW interface-entries struct exposes. +/// +internal static class WellKnownInterfaceEntriesEmitter +{ + /// + /// Emits the well-known interface entries for a delegate's reference interface entries. + /// Lines are written in the order expected by DelegateReferenceInterfaceEntries. + /// + /// The writer to emit to. + public static void EmitDelegateReferenceWellKnownEntries(IndentedTextWriter writer) + { + writer.WriteLine(""" + Entries.IPropertyValue.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IPropertyValue; + Entries.IPropertyValue.Vtable = global::WindowsRuntime.InteropServices.IPropertyValueImpl.OtherTypeVtable; + Entries.IStringable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IStringable; + Entries.IStringable.Vtable = global::WindowsRuntime.InteropServices.IStringableImpl.Vtable; + Entries.IWeakReferenceSource.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IWeakReferenceSource; + Entries.IWeakReferenceSource.Vtable = global::WindowsRuntime.InteropServices.IWeakReferenceSourceImpl.Vtable; + Entries.IMarshal.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IMarshal; + Entries.IMarshal.Vtable = global::WindowsRuntime.InteropServices.IMarshalImpl.Vtable; + Entries.IAgileObject.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IAgileObject; + Entries.IAgileObject.Vtable = global::WindowsRuntime.InteropServices.IAgileObjectImpl.Vtable; + Entries.IInspectable.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IInspectable; + Entries.IInspectable.Vtable = global::WindowsRuntime.InteropServices.IInspectableImpl.Vtable; + Entries.IUnknown.IID = global::WindowsRuntime.InteropServices.WellKnownInterfaceIIDs.IID_IUnknown; + Entries.IUnknown.Vtable = global::WindowsRuntime.InteropServices.IUnknownImpl.Vtable; + """, isMultiline: true); + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Helpers/WindowsMetadataExpander.cs b/src/WinRT.Projection.Writer/Helpers/WindowsMetadataExpander.cs similarity index 88% rename from src/WinRT.Projection.Generator.Writer/Helpers/WindowsMetadataExpander.cs rename to src/WinRT.Projection.Writer/Helpers/WindowsMetadataExpander.cs index 7343d8322e..71269a79e8 100644 --- a/src/WinRT.Projection.Generator.Writer/Helpers/WindowsMetadataExpander.cs +++ b/src/WinRT.Projection.Writer/Helpers/WindowsMetadataExpander.cs @@ -4,17 +4,17 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security; using System.Text.RegularExpressions; using System.Xml; using Microsoft.Win32; +using WindowsRuntime.ProjectionWriter.Errors; -namespace WindowsRuntime.ProjectionGenerator.Writer; +namespace WindowsRuntime.ProjectionWriter.Helpers; /// -/// 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. +/// Expands a Windows metadata token (e.g. "sdk", "sdk+", "local", +/// "10.0.26100.0", or a literal path) into the set of .winmd files for that input. /// public static partial class WindowsMetadataExpander { @@ -53,10 +53,12 @@ public static List Expand(string token) 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; } @@ -72,6 +74,7 @@ public static List Expand(string token) else { Match m = SdkVersionRegex.Match(token); + if (m.Success) { sdkVersion = m.Groups[1].Value; @@ -81,9 +84,10 @@ public static List Expand(string token) if (!string.IsNullOrEmpty(sdkVersion)) { string sdkPath = TryGetSdkPath(); + if (string.IsNullOrEmpty(sdkPath)) { - throw new InvalidOperationException("Could not find the Windows SDK in the registry."); + throw WellKnownProjectionWriterExceptions.WindowsSdkNotFound(); } string platformXml = Path.Combine(sdkPath, "Platforms", "UAP", sdkVersion, "Platform.xml"); @@ -92,11 +96,13 @@ public static List Expand(string token) 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); @@ -113,46 +119,52 @@ public static List Expand(string token) 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. + // Try the WOW64 view first (the SDK installer registers under the 32-bit hive), 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 + + // Both views can fail with permission errors on hardened machines, or with I/O errors + // when the registry hive is being modified concurrently by an installer. Treat any of + // those as "no SDK detected" and let the caller fall back to the path-not-found error. + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException) { - // 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; @@ -164,15 +176,19 @@ private static string TryGetCurrentSdkVersion() 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; @@ -181,13 +197,11 @@ private static string TryGetCurrentSdkVersion() } 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}."); + throw WellKnownProjectionWriterExceptions.CannotReadWindowsSdkXml(xmlPath); } XmlReaderSettings settings = new() { DtdProcessing = DtdProcessing.Ignore, IgnoreWhitespace = true }; @@ -198,12 +212,15 @@ private static void AddFilesFromPlatformXml(List result, string sdkVersi { 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.Writer/Metadata/MetadataCache.cs similarity index 64% rename from src/WinRT.Projection.Generator.Writer/Metadata/MetadataCache.cs rename to src/WinRT.Projection.Writer/Metadata/MetadataCache.cs index 0a80e3d0f5..95769f0733 100644 --- a/src/WinRT.Projection.Generator.Writer/Metadata/MetadataCache.cs +++ b/src/WinRT.Projection.Writer/Metadata/MetadataCache.cs @@ -6,22 +6,36 @@ using System.IO; using System.Linq; using AsmResolver.DotNet; +using WindowsRuntime.ProjectionWriter.Errors; -namespace WindowsRuntime.ProjectionGenerator.Writer; +namespace WindowsRuntime.ProjectionWriter.Metadata; /// -/// 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(); + /// Backing field for . + private readonly Dictionary _namespaces = []; + /// Backing field for the global type-by-full-name index used by . + private readonly Dictionary _typesByFullName = []; + + /// Backing field for the type-to-source-module-path index used by . + private readonly Dictionary _typeToModulePath = []; + + /// Backing field for . + private readonly List _modules = []; + + /// + /// Gets the loaded namespaces, keyed by namespace name. Each value bag holds the + /// per-category type lists ( + per-kind splits). + /// public IReadOnlyDictionary Namespaces => _namespaces; + /// + /// Gets the loaded modules in load order. + /// public IReadOnlyList Modules => _modules; /// @@ -36,6 +50,11 @@ private MetadataCache(RuntimeContext runtimeContext) RuntimeContext = runtimeContext; } + /// + /// Loads the input .winmd files (or directories of .winmd files) into a new metadata cache. + /// + /// The input paths. + /// The loaded metadata cache. public static MetadataCache Load(IEnumerable inputs) { // Collect all .winmd files first so the resolver knows about all of them. Dedupe by canonical @@ -44,8 +63,9 @@ public static MetadataCache Load(IEnumerable inputs) // 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(); + int capacity = inputs is ICollection collection ? collection.Count : 0; + HashSet seen = new(capacity, StringComparer.OrdinalIgnoreCase); + List winmdFiles = []; foreach (string input in inputs) { if (Directory.Exists(input)) @@ -53,6 +73,7 @@ public static MetadataCache Load(IEnumerable inputs) foreach (string path in Directory.EnumerateFiles(input, "*.winmd", SearchOption.AllDirectories)) { string canonical = Path.GetFullPath(path); + if (seen.Add(canonical)) { winmdFiles.Add(canonical); @@ -62,6 +83,7 @@ public static MetadataCache Load(IEnumerable inputs) else if (File.Exists(input)) { string canonical = Path.GetFullPath(input); + if (seen.Add(canonical)) { winmdFiles.Add(canonical); @@ -69,10 +91,14 @@ public static MetadataCache Load(IEnumerable inputs) } else { - throw new FileNotFoundException($"Input metadata file/directory not found: {input}", input); + throw WellKnownProjectionWriterExceptions.InvalidInputPath(input); } } + // Sort the file list (case-insensitive ordinal) so first-load-wins behavior in LoadFile + // is deterministic regardless of filesystem enumeration order. + winmdFiles.Sort(StringComparer.OrdinalIgnoreCase); + // 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 @@ -92,26 +118,26 @@ public static MetadataCache Load(IEnumerable inputs) 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. + /// Sorts each namespace's list alphabetically by type name + /// so all downstream iteration produces deterministic output. /// 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); + static int Compare(TypeDefinition a, TypeDefinition b) => 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); @@ -126,17 +152,18 @@ private void SortMembersByName() 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}'."); + throw WellKnownProjectionWriterExceptions.MalformedWinmd(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; + (string ns, string name) = type.Names(); // Skip the pseudo-type if (name == "") @@ -147,12 +174,10 @@ private void LoadFile(string path) // 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"). + // contract winmd and a 3rd-party WinMD that re-exports / forwards them). First-load-wins. string fullName = string.IsNullOrEmpty(ns) ? name : ns + "." + name; - if (_typesByFullName.ContainsKey(fullName)) + + if (!_typesByFullName.TryAdd(fullName, type)) { continue; } @@ -162,9 +187,9 @@ private void LoadFile(string path) members = new NamespaceMembers(ns); _namespaces[ns] = members; } + members.AddType(type); - _typesByFullName[fullName] = type; _typeToModulePath[type] = moduleFilePath; } } @@ -190,67 +215,6 @@ public string GetSourcePath(TypeDefinition type) /// 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; - } + return Find(fullName) ?? throw WellKnownProjectionWriterExceptions.CannotResolveType(fullName); } } diff --git a/src/WinRT.Projection.Writer/Metadata/NamespaceMembers.cs b/src/WinRT.Projection.Writer/Metadata/NamespaceMembers.cs new file mode 100644 index 0000000000..3311ebb791 --- /dev/null +++ b/src/WinRT.Projection.Writer/Metadata/NamespaceMembers.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionWriter.Metadata; + +/// +/// The types in a particular namespace, organized by category. +/// +/// The name of the namespace. +internal sealed class NamespaceMembers(string name) +{ + /// Gets the namespace name (e.g. Windows.Foundation). + public string Name { get; } = name; + + /// Gets the flat list of every type declared in this namespace, in load + sort order. + public List Types { get; } = []; + + /// Gets the interface-category types declared in this namespace. + public List Interfaces { get; } = []; + + /// Gets the runtime-class-category types (excluding attribute classes) declared in this namespace. + public List Classes { get; } = []; + + /// Gets the enum-category types declared in this namespace. + public List Enums { get; } = []; + + /// Gets the struct-category types (excluding API-contract markers) declared in this namespace. + public List Structs { get; } = []; + + /// Gets the delegate-category types declared in this namespace. + public List Delegates { get; } = []; + + /// Gets the attribute-class types declared in this namespace. + public List Attributes { get; } = []; + + /// Gets the API-contract marker types declared in this namespace (a struct sub-category). + public List Contracts { get; } = []; + + /// + /// Adds to and to the matching per-category bucket. + /// + /// The type definition to add. + 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.Writer/Metadata/TypeCategorization.cs similarity index 71% rename from src/WinRT.Projection.Generator.Writer/Metadata/TypeCategorization.cs rename to src/WinRT.Projection.Writer/Metadata/TypeCategorization.cs index f49db2924d..bd6ea5c02a 100644 --- a/src/WinRT.Projection.Generator.Writer/Metadata/TypeCategorization.cs +++ b/src/WinRT.Projection.Writer/Metadata/TypeCategorization.cs @@ -3,11 +3,13 @@ using AsmResolver; using AsmResolver.DotNet; +using static WindowsRuntime.ProjectionWriter.References.WellKnownAttributeNames; +using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; -namespace WindowsRuntime.ProjectionGenerator.Writer; +namespace WindowsRuntime.ProjectionWriter.Metadata; /// -/// Mirrors the C++ category enum in winmd::reader::category. +/// Categorization of a Windows Runtime type definition. /// internal enum TypeCategory { @@ -20,46 +22,57 @@ internal enum TypeCategory /// /// 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). + /// + /// 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. + /// + /// 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) @@ -68,6 +81,7 @@ public static bool IsAttributeType(TypeDefinition type) { 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; @@ -75,52 +89,67 @@ public static bool IsAttributeType(TypeDefinition type) return false; } - /// True if this is an API contract struct type. + /// + /// 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"); + HasAttribute(type, WindowsFoundationMetadata, "ApiContractAttribute"); } - /// True if this type is a static class (abstract+sealed). + /// + /// 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]. + /// + /// 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"); + HasAttribute(type, WindowsFoundationMetadata, ExclusiveToAttribute); } - /// True if this is a [Flags] enum. + /// + /// 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). + /// + /// 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]. + /// + /// True if this type is marked [ProjectionInternal]. + /// public static bool IsProjectionInternal(TypeDefinition type) { - return HasAttribute(type, "WindowsRuntime.Internal", "ProjectionInternalAttribute"); + return HasAttribute(type, WindowsRuntimeInternal, "ProjectionInternalAttribute"); } - /// True if this type's CustomAttributes contains the given attribute. + /// + /// 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; @@ -129,13 +158,16 @@ public static bool HasAttribute(IHasCustomAttribute member, string ns, string na return false; } - /// Gets the matching CustomAttribute or null. + /// + /// 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; diff --git a/src/WinRT.Projection.Writer/Metadata/TypeSemantics.cs b/src/WinRT.Projection.Writer/Metadata/TypeSemantics.cs new file mode 100644 index 0000000000..1857544bf1 --- /dev/null +++ b/src/WinRT.Projection.Writer/Metadata/TypeSemantics.cs @@ -0,0 +1,320 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.Errors; + +namespace WindowsRuntime.ProjectionWriter.Metadata; + +/// +/// Identifies a fundamental WinRT primitive type (those whose ABI representation matches a C# +/// primitive type, plus ). +/// +internal enum FundamentalType +{ + /// . + Boolean, + + /// . + Char, + + /// . + Int8, + + /// . + UInt8, + + /// . + Int16, + + /// . + UInt16, + + /// . + Int32, + + /// . + UInt32, + + /// . + Int64, + + /// . + UInt64, + + /// . + Float, + + /// . + Double, + + /// . + String, +} + +/// +/// Discriminated union of WinRT type semantics produced by . +/// +internal abstract record TypeSemantics +{ + private TypeSemantics() { } + + /// + /// A fundamental WinRT primitive (see ). + /// + /// The underlying fundamental type. + public sealed record Fundamental(FundamentalType Type) : TypeSemantics; + + /// + /// The corlib type. + /// + public sealed record ObjectType : TypeSemantics; + + /// + /// The corlib type. + /// + public sealed record GuidType : TypeSemantics; + + /// + /// The corlib type. + /// + public sealed record SystemType : TypeSemantics; + + /// + /// A WinRT class / interface / struct / enum / delegate defined in the loaded metadata. + /// + /// The type definition. + public sealed record Definition(TypeDefinition Type) : TypeSemantics; + + /// + /// A closed generic instantiation whose generic type is resolved. + /// + /// The open generic type definition. + /// The instantiation arguments. + public sealed record GenericInstance(TypeDefinition GenericType, List GenericArgs) : TypeSemantics; + + /// + /// A closed generic instantiation whose generic type is referenced (cross-assembly). + /// + /// The open generic type reference. + /// The instantiation arguments. + public sealed record GenericInstanceRef(ITypeDefOrRef GenericType, List GenericArgs) : TypeSemantics; + + /// + /// A reference to a type generic parameter at the specified index. + /// + /// The zero-based parameter index. + public sealed record GenericTypeIndex(int Index) : TypeSemantics; + + /// + /// A reference to a method generic parameter at the specified index. + /// + /// The zero-based parameter index. + public sealed record GenericMethodIndex(int Index) : TypeSemantics; + + /// + /// A bound generic parameter token (rare; appears in nested generics). + /// + /// The generic parameter. + public sealed record BoundGenericParameter(GenericParameter Parameter) : TypeSemantics; + + /// + /// A reference to a type defined in another assembly. + /// + /// The type reference. + /// Whether the reference points at a value type (struct/enum) or a reference type. + public sealed record Reference(TypeReference Type, bool IsValueType = false) : TypeSemantics; +} + +/// +/// Static helpers for converting AsmResolver type signatures into instances. +/// +internal static class TypeSemanticsFactory +{ + /// + /// Resolves into the discriminated + /// shape used by the writer's emission paths. + /// + /// The AsmResolver type signature to convert. + /// The corresponding case. + 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 WellKnownProjectionWriterExceptions.UnsupportedTypeSignature(signature?.ToString() ?? "")), + }; + } + + /// + /// Resolves an into the corresponding . + /// Recognizes the special-cased corlib types (, , + /// ) and falls back to / + /// for everything else. + /// + /// The type def-or-ref to convert. + /// Whether the type def-or-ref is known to be a value type (only used for the case). + /// The corresponding case. + 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, string name) = reference.Names(); + + if (ns == "System" && name == "Guid") + { + return new TypeSemantics.GuidType(); + } + + if (ns == "System" && name == "Object") + { + return new TypeSemantics.ObjectType(); + } + + if (ns == "System" && name == "Type") + { + return new TypeSemantics.SystemType(); + } + + 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.ObjectType(), + _ => throw WellKnownProjectionWriterExceptions.UnsupportedCorLibElementType(elementType) + }; + } + + [SuppressMessage("Style", "IDE0028:Use collection expression", + Justification = "List(capacity) cannot be expressed as a collection expression.")] + private static TypeSemantics GetGenericInstance(GenericInstanceTypeSignature gi) + { + ITypeDefOrRef genericType = gi.GenericType; + // Always preserve the type arguments. + List args = new(gi.TypeArguments.Count); + foreach (TypeSignature arg in gi.TypeArguments) + { + args.Add(Get(arg)); + } + + if (genericType is not TypeDefinition def) + { + // 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. +/// +internal enum TypedefNameType +{ + Projected, + CCW, + ABI, + NonProjected, + StaticAbiClass, + EventSource, + Marshaller, + ArrayMarshaller, + InteropIID, +} + +/// +/// Maps the abstract enum (a closed set of WinRT +/// fundamental types: bool/char/numeric/string) to its projected C# type name and +/// .NET reflection name. +/// +internal static class FundamentalTypes +{ + /// + /// Returns the C# keyword form for (e.g. "int", "string"), + /// or "object" for unrecognized cases. + /// + /// The fundamental type to format. + /// The C# keyword. + 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" + }; + + /// + /// Returns the .NET reflection short name for (e.g. "Int32", + /// "String"), or "Object" for unrecognized cases. + /// + /// The fundamental type to format. + /// The .NET reflection short name. + 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.Writer/Models/AbiTypeShape.cs b/src/WinRT.Projection.Writer/Models/AbiTypeShape.cs new file mode 100644 index 0000000000..bc23111db8 --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/AbiTypeShape.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Immutable record describing the ABI marshalling shape of a single WinRT type signature +/// (the kind of marshalling needed, plus the underlying type signature). +/// +/// The classification of the type signature's ABI shape. +/// The original type signature this shape was derived from. +internal sealed record AbiTypeShape(AbiTypeShapeKind Kind, TypeSignature Signature); \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Models/AbiTypeShapeKind.cs b/src/WinRT.Projection.Writer/Models/AbiTypeShapeKind.cs new file mode 100644 index 0000000000..e4140cc42a --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/AbiTypeShapeKind.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Classification of a WinRT ABI type's marshalling shape, used to drive the writer's +/// decisions about how to emit converters and parameter handling. +/// +internal enum AbiTypeShapeKind +{ + /// + /// The shape could not be determined (e.g. unresolved reference). + /// + Unknown, + + /// + /// A C# primitive type that is directly representable at the ABI (bool/byte/short/int/long/float/double/char + unsigned variants). + /// + BlittablePrimitive, + + /// + /// A WinRT enum (marshalled as its underlying integer at the ABI). + /// + Enum, + + /// + /// A WinRT struct whose fields are all blittable primitives or other blittable structs (no per-field marshalling needed). + /// + BlittableStruct, + + /// + /// A WinRT struct with at least one reference-type field (string, generic instance, runtime class, etc.) that needs per-field marshalling via a generated *Marshaller. + /// + ComplexStruct, + + /// + /// A WinRT struct that is mapped to a BCL value type and requires explicit marshalling (e.g. Windows.Foundation.DateTime -> ). + /// + MappedAbiValueType, + + /// + /// The corlib primitive (marshalled via HSTRING). + /// + String, + + /// + /// The corlib primitive (marshalled via WindowsRuntimeObjectMarshaller). + /// + Object, + + /// + /// A WinRT runtime class or interface (marshalled via the type's generated *Marshaller). + /// + RuntimeClassOrInterface, + + /// + /// A WinRT delegate type (marshalled via the delegate's generated *Marshaller). + /// + Delegate, + + /// + /// A generic instantiation that requires WinRT.Interop UnsafeAccessor-based marshalling. + /// + GenericInstance, + + /// + /// A / WinRT IReference<T> instantiation. + /// + NullableT, + + /// + /// The projected type (mapped from the WinMD Windows.UI.Xaml.Interop.TypeName struct). + /// + SystemType, + + /// + /// The / Windows.Foundation.HResult pair, marshalled via ABI.System.ExceptionMarshaller. + /// + HResultException, + + /// + /// A single-dimensional array. + /// + Array, +} diff --git a/src/WinRT.Projection.Writer/Models/MethodSignatureInfo.cs b/src/WinRT.Projection.Writer/Models/MethodSignatureInfo.cs new file mode 100644 index 0000000000..a7de374bc4 --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/MethodSignatureInfo.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.ProjectionWriter.References; + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Resolved view of a method definition's signature: the method itself, the per-parameter info +/// (with any active generic-context substitutions already applied), the optional return parameter, +/// and the substituted return type. +/// +internal sealed class MethodSignatureInfo +{ + /// + /// Gets the underlying method definition. + /// + public MethodDefinition Method { get; } + + /// + /// Gets the per-parameter info for the method, in declaration order. + /// + public IReadOnlyList Parameters => _params; + + private readonly List _params; + + /// + /// Gets the parameter definition with sequence 0 (the return parameter), or + /// if the method does not have one. + /// + public ParameterDefinition? ReturnParameter { get; } + + /// + /// Gets the (possibly generic-context-substituted) return type of the method, or + /// when the method returns . + /// + public TypeSignature? ReturnType { get; } + + /// + /// Initializes a new with no generic context. + /// + /// The method definition to wrap. + public MethodSignatureInfo(MethodDefinition method) : this(method, null) { } + + /// + /// Initializes a new . + /// + /// The method definition to wrap. + /// An optional generic context used to substitute generic parameters in the parameter and return types. + [SuppressMessage("Style", "IDE0028:Use collection expression", + Justification = "List(capacity) cannot be expressed as a collection expression.")] + public MethodSignatureInfo(MethodDefinition method, GenericContext? genericContext) + { + Method = method; + _params = new List(method.Parameters.Count); + ReturnParameter = null; + foreach (ParameterDefinition parameter in method.ParameterDefinitions) + { + if (parameter.Sequence == 0) + { + ReturnParameter = parameter; + break; + } + } + + if (method.Signature is MethodSignature sig) + { + TypeSignature? rt = sig.ReturnType; + + if (rt is not null && genericContext is not null) + { + rt = rt.InstantiateGenericTypes(genericContext.Value); + } + + ReturnType = rt is CorLibTypeSignature { ElementType: ElementType.Void } ? null : rt; + + for (int i = 0; i < sig.ParameterTypes.Count; i++) + { + TypeSignature pt = sig.ParameterTypes[i]; + + if (genericContext is not null) + { + pt = pt.InstantiateGenericTypes(genericContext.Value); + } + + _params.Add(new ParameterInfo(method.Parameters[i], pt)); + } + } + } + + /// + /// Returns the name of the return parameter, or if there is none. + /// + /// The default name to use when no return parameter is declared. + /// The return parameter name (or default). + public string ReturnParameterName(string defaultName = ProjectionNames.DefaultReturnParameterName) + => ReturnParameter?.Name?.Value ?? defaultName; +} diff --git a/src/WinRT.Projection.Writer/Models/ParameterCategory.cs b/src/WinRT.Projection.Writer/Models/ParameterCategory.cs new file mode 100644 index 0000000000..d4ad90e5cb --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/ParameterCategory.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Categorization of how a parameter is passed across the WinRT ABI boundary. +/// +internal enum ParameterCategory +{ + /// + /// By-value input parameter (the default). + /// + In, + + /// + /// By-reference parameter (read/write). + /// + Ref, + + /// + /// By-reference output-only parameter. + /// + Out, + + /// + /// An input array passed by value (caller fills the array). + /// + PassArray, + + /// + /// An array of fixed length whose contents the callee fills. + /// + FillArray, + + /// + /// An output array allocated by the callee and returned to the caller. + /// + ReceiveArray, +} diff --git a/src/WinRT.Projection.Writer/Models/ParameterInfo.cs b/src/WinRT.Projection.Writer/Models/ParameterInfo.cs new file mode 100644 index 0000000000..c1da38849b --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/ParameterInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Collections; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Information about a single method parameter, pairing the metadata +/// definition (which carries its name, in/out flags, default value, and custom attributes) +/// with the resolved after any active generic-context substitutions. +/// +/// The metadata parameter definition. +/// The signature type (with generic substitutions already applied). +internal sealed record ParameterInfo(Parameter Parameter, TypeSignature Type); diff --git a/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs b/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs new file mode 100644 index 0000000000..999013a472 --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Per-property state captured while walking the members of a runtime class so that the getter +/// and setter (which may come from different interfaces, with different platform attributes +/// and ABI Methods classes) can be reconciled into a single C# property declaration. +/// +/// +/// The first nine fields below mirror in name and order +/// (so the two state bags can be diffed side-by-side); the remaining fields hold instance-only +/// data (accessibility/specifier text, generic-instantiation accessor info, overridable-interface +/// metadata) that does not apply to static properties. +/// +internal sealed class PropertyAccessorState +{ + /// + /// Gets or sets whether a getter accessor has been seen for this property. + /// + public bool HasGetter { get; set; } + + /// + /// Gets or sets whether a setter accessor has been seen for this property. + /// + public bool HasSetter { get; set; } + + /// + /// Gets or sets the projected C# type text of the property (for the unified getter+setter declaration). + /// + public string PropTypeText { get; set; } = string.Empty; + + /// + /// Gets or sets the ABI Methods class name used by the getter dispatch. + /// + public string GetterAbiClass { get; set; } = string.Empty; + + /// + /// Gets or sets the field name of the _objRef_ the getter dispatches through. + /// + public string GetterObjRef { get; set; } = string.Empty; + + /// + /// Gets or sets the ABI Methods class name used by the setter dispatch. + /// + public string SetterAbiClass { get; set; } = string.Empty; + + /// + /// Gets or sets the field name of the _objRef_ the setter dispatches through. + /// + public string SetterObjRef { get; set; } = string.Empty; + + /// + /// Gets or sets the platform-attribute string for the getter (in reference-projection mode, + /// emitted before the property when both accessors share a platform; otherwise per-accessor). + /// + public string GetterPlatformAttribute { get; set; } = string.Empty; + + /// + /// Gets or sets the platform-attribute string for the setter (in reference-projection mode, + /// emitted before the property when both accessors share a platform; otherwise per-accessor). + /// + public string SetterPlatformAttribute { get; set; } = string.Empty; + + // -- Instance-only fields below (not present on StaticPropertyAccessorState) -- + + /// + /// Gets or sets the C# accessibility modifier text (e.g. "public "). + /// + public string Access { get; set; } = "public "; + + /// + /// Gets or sets the method-spec modifier text (e.g. "override ", "new "). + /// + public string MethodSpec { get; set; } = string.Empty; + + /// + /// Gets or sets the property name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets whether the getter dispatches through a generic-instantiation marshaller. + /// + public bool GetterIsGeneric { get; set; } + + /// + /// Gets or sets whether the setter dispatches through a generic-instantiation marshaller. + /// + public bool SetterIsGeneric { get; set; } + + /// + /// Gets or sets the interop type name string used by the getter's UnsafeAccessor. + /// + public string GetterGenericInteropType { get; set; } = string.Empty; + + /// + /// Gets or sets the accessor name used for the getter's UnsafeAccessor. + /// + public string GetterGenericAccessorName { get; set; } = string.Empty; + + /// + /// Gets or sets the projected property type text used by the getter dispatch. + /// + public string GetterPropTypeText { get; set; } = string.Empty; + + /// + /// Gets or sets the interop type name string used by the setter's UnsafeAccessor. + /// + public string SetterGenericInteropType { get; set; } = string.Empty; + + /// + /// Gets or sets the accessor name used for the setter's UnsafeAccessor. + /// + public string SetterGenericAccessorName { get; set; } = string.Empty; + + /// + /// Gets or sets the projected property type text used by the setter dispatch. + /// + public string SetterPropTypeText { get; set; } = string.Empty; + + /// + /// Gets or sets whether this property comes from an [Overridable] interface (and so + /// needs an explicit interface implementation). + /// + public bool IsOverridable { get; set; } + + /// + /// Gets or sets the originating interface (used to qualify the explicit interface implementation + /// when is set). + /// + public ITypeDefOrRef? OverridableInterface { get; set; } +} diff --git a/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs b/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs new file mode 100644 index 0000000000..160337221b --- /dev/null +++ b/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.Models; + +/// +/// Per-property state captured while walking the static members of a runtime class so that the +/// static getter and setter can be reconciled into a single static C# property declaration. +/// +internal sealed class StaticPropertyAccessorState +{ + /// + /// Gets or sets whether a static getter accessor has been seen for this property. + /// + public bool HasGetter { get; set; } + + /// + /// Gets or sets whether a static setter accessor has been seen for this property. + /// + public bool HasSetter { get; set; } + + /// + /// Gets or sets the projected C# type text of the property (for the unified getter+setter declaration). + /// + public string PropTypeText { get; set; } = string.Empty; + + /// + /// Gets or sets the ABI Methods class name used by the getter dispatch. + /// + public string GetterAbiClass { get; set; } = string.Empty; + + /// + /// Gets or sets the field name of the _objRef_ the getter dispatches through. + /// + public string GetterObjRef { get; set; } = string.Empty; + + /// + /// Gets or sets the ABI Methods class name used by the setter dispatch. + /// + public string SetterAbiClass { get; set; } = string.Empty; + + /// + /// Gets or sets the field name of the _objRef_ the setter dispatches through. + /// + public string SetterObjRef { get; set; } = string.Empty; + + /// + /// Gets or sets the platform-attribute string for the getter (emitted before the property when + /// both accessors share a platform; otherwise per-accessor). + /// + public string GetterPlatformAttribute { get; set; } = string.Empty; + + /// + /// Gets or sets the platform-attribute string for the setter (emitted before the property when + /// both accessors share a platform; otherwise per-accessor). + /// + public string SetterPlatformAttribute { get; set; } = string.Empty; +} diff --git a/src/WinRT.Projection.Writer/ProjectionWriter.cs b/src/WinRT.Projection.Writer/ProjectionWriter.cs new file mode 100644 index 0000000000..53fc163efd --- /dev/null +++ b/src/WinRT.Projection.Writer/ProjectionWriter.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using WindowsRuntime.ProjectionWriter.Errors; +using WindowsRuntime.ProjectionWriter.Generation; +using WindowsRuntime.ProjectionWriter.Metadata; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Public API for generating C# Windows Runtime projections from .winmd metadata. +/// +/// 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. Generates C# projections for the input WinRT metadata and writes them to the configured output folder. + /// + /// 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 WellKnownProjectionWriterExceptions.MissingInputPaths(); + } + + if (string.IsNullOrEmpty(options.OutputFolder)) + { + throw WellKnownProjectionWriterExceptions.MissingOutputFolder(); + } + + Settings settings; + + // Translate the public options into the internal settings bag. + try + { + settings = new() + { + Verbose = options.Verbose, + Logger = options.Logger, + MaxDegreesOfParallelism = options.MaxDegreesOfParallelism, + 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), + }; + + settings.Input.UnionWith(options.InputPaths); + settings.Include.UnionWith(options.Include); + settings.Exclude.UnionWith(options.Exclude); + settings.AdditionExclude.UnionWith(options.AdditionExclude); + + settings.MakeReadOnly(); + + _ = Directory.CreateDirectory(settings.OutputFolder); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledProjectionWriterException("parsing", e); + } + + options.CancellationToken.ThrowIfCancellationRequested(); + + MetadataCache cache; + + // Load the metadata cache from the configured input paths. + try + { + cache = MetadataCache.Load(settings.Input); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledProjectionWriterException("metadata-load", e); + } + + options.CancellationToken.ThrowIfCancellationRequested(); + + // Run the generator. Phase boundaries within the orchestrator wrap their own + // discovery / emit blocks with UnhandledProjectionWriterException. + ProjectionGenerator generator = new(settings, cache, options.CancellationToken); + generator.Run(); + } +} diff --git a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs new file mode 100644 index 0000000000..9a03dc9332 --- /dev/null +++ b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Configuration bag passed to . +/// Specifies the input .winmd metadata, the output folder, namespace include / exclude +/// filters, and per-projection-mode toggles (component authoring, reference-only projection, +/// public enums, etc.). +/// +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; } = []; + + /// + /// Optional list of namespace prefixes to exclude from the projection. + /// + public IReadOnlyList Exclude { get; init; } = []; + + /// + /// Optional list of namespace prefixes to exclude from the projection additions. + /// + public IReadOnlyList AdditionExclude { get; init; } = []; + + /// + /// 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; } + + /// + /// Optional logger callback invoked for each verbose progress message (only used when + /// is ). Defaults to , + /// in which case verbose messages are forwarded to . + /// + public Action? Logger { get; init; } + + /// + /// Maximum number of parallel work items to dispatch when generating projections. + /// Defaults to -1, which lets the runtime pick (typically ). + /// Set to 1 to force fully sequential execution (useful for debugging or when a deterministic + /// thread schedule is required). + /// + public int MaxDegreesOfParallelism { get; init; } = -1; + + /// + /// Gets the cancellation token observed during projection generation. Defaults to + /// , which never signals cancellation. + /// + public CancellationToken CancellationToken { get; init; } +} diff --git a/src/WinRT.Projection.Writer/References/ProjectionNames.cs b/src/WinRT.Projection.Writer/References/ProjectionNames.cs new file mode 100644 index 0000000000..39318e768e --- /dev/null +++ b/src/WinRT.Projection.Writer/References/ProjectionNames.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.References; + +/// +/// Common string literals used throughout the projection writer (prefixes, suffixes, conventions), +/// centralized so they have a single canonical source. +/// +internal static class ProjectionNames +{ + /// + /// The C# global namespace prefix ("global::"). + /// + public const string GlobalPrefix = "global::"; + + /// + /// The ABI namespace prefix ("ABI."). + /// + public const string AbiPrefix = "ABI."; + + /// + /// The fully-qualified ABI namespace prefix ("global::ABI."). + /// + public const string GlobalAbiPrefix = "global::ABI."; + + /// + /// The suffix appended to ABI marshaller class names ("Marshaller"). + /// + public const string MarshallerSuffix = "Marshaller"; + + /// + /// The conventional name of the managed-to-native marshaller method ("ConvertToUnmanaged"). + /// + public const string ConvertToUnmanaged = "ConvertToUnmanaged"; + + /// + /// The conventional name of the native-to-managed marshaller method ("ConvertToManaged"). + /// + public const string ConvertToManaged = "ConvertToManaged"; + + /// + /// The conventional name of the static IID property on a projected interface ("IID"). + /// + public const string IID = "IID"; + + /// + /// The C# void-pointer keyword form ("void*"). + /// + public const string VoidPointer = "void*"; + + /// + /// The default placeholder name used for a method's return parameter when the + /// metadata does not declare one explicitly ("__return_value__"). + /// + public const string DefaultReturnParameterName = "__return_value__"; +} diff --git a/src/WinRT.Projection.Writer/References/WellKnownAttributeNames.cs b/src/WinRT.Projection.Writer/References/WellKnownAttributeNames.cs new file mode 100644 index 0000000000..083c22b5fe --- /dev/null +++ b/src/WinRT.Projection.Writer/References/WellKnownAttributeNames.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.References; + +/// +/// Well-known Windows.Foundation.Metadata attribute type names recognized by the +/// projection writer. +/// +internal static class WellKnownAttributeNames +{ + /// + /// The [ActivatableAttribute] attribute type name. + /// + public const string ActivatableAttribute = "ActivatableAttribute"; + + /// + /// The [StaticAttribute] attribute type name. + /// + public const string StaticAttribute = "StaticAttribute"; + + /// + /// The [ComposableAttribute] attribute type name. + /// + public const string ComposableAttribute = "ComposableAttribute"; + + /// + /// The [DefaultAttribute] attribute type name (marks the default interface of a runtime class). + /// + public const string DefaultAttribute = "DefaultAttribute"; + + /// + /// The [OverridableAttribute] attribute type name (marks an overridable interface). + /// + public const string OverridableAttribute = "OverridableAttribute"; + + /// + /// The [ExclusiveToAttribute] attribute type name (marks an interface as exclusive to a class). + /// + public const string ExclusiveToAttribute = "ExclusiveToAttribute"; + + /// + /// The [FastAbiAttribute] attribute type name (enables the merged fast-abi vtable layout). + /// + public const string FastAbiAttribute = "FastAbiAttribute"; + + /// + /// The [NoExceptionAttribute] attribute type name (marks a method/property as non-throwing). + /// + public const string NoExceptionAttribute = "NoExceptionAttribute"; + + /// + /// The [ContractVersionAttribute] attribute type name. + /// + public const string ContractVersionAttribute = "ContractVersionAttribute"; + + /// + /// The [VersionAttribute] attribute type name. + /// + public const string VersionAttribute = "VersionAttribute"; +} diff --git a/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs b/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs new file mode 100644 index 0000000000..333546ce6f --- /dev/null +++ b/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.References; + +/// +/// Well-known WinRT metadata namespaces. +/// +internal static class WellKnownNamespaces +{ + /// + /// The Windows.Foundation namespace. + /// + public const string WindowsFoundation = "Windows.Foundation"; + + /// + /// The Windows.Foundation.Metadata namespace where WinRT metadata attributes live. + /// + public const string WindowsFoundationMetadata = "Windows.Foundation.Metadata"; + + /// + /// The Windows.Foundation.Collections namespace. + /// + public const string WindowsFoundationCollections = "Windows.Foundation.Collections"; + + /// + /// The System namespace (BCL primitives + special types). + /// + public const string System = "System"; + + /// + /// The WindowsRuntime.Internal namespace (internal interop interfaces). + /// + public const string WindowsRuntimeInternal = "WindowsRuntime.Internal"; + + /// + /// The Windows.UI.Xaml.Interop namespace. + /// + public const string WindowsUIXamlInterop = "Windows.UI.Xaml.Interop"; +} diff --git a/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs b/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs new file mode 100644 index 0000000000..5293344e58 --- /dev/null +++ b/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.ProjectionWriter.References; + +/// +/// Well-known WinRT type names referenced by the projection writer. +/// +internal static class WellKnownTypeNames +{ + /// + /// The WinRT HResult struct (mapped to ). + /// + public const string HResult = "HResult"; + + /// + /// The WinRT DateTime struct (mapped to ). + /// + public const string DateTime = "DateTime"; + + /// + /// The WinRT TimeSpan struct (mapped to ). + /// + public const string TimeSpan = "TimeSpan"; + + /// + /// The WinRT IReference<T> generic interface (mapped to ). + /// + public const string IReferenceGeneric = "IReference`1"; + + /// + /// The BCL Nullable<T> generic struct. + /// + public const string NullableGeneric = "Nullable`1"; + + /// + /// The BCL type name. + /// + public const string Type = "Type"; + + /// + /// The BCL type name. + /// + public const string Exception = "Exception"; + + /// + /// The BCL type name. + /// + public const string Object = "Object"; + + /// + /// The Windows SDK XAML TypeName struct (the WinMD source for ). + /// + public const string TypeName = "TypeName"; +} diff --git a/src/WinRT.Projection.Writer/Resolvers/AbiTypeShapeResolver.cs b/src/WinRT.Projection.Writer/Resolvers/AbiTypeShapeResolver.cs new file mode 100644 index 0000000000..31c92c8bc2 --- /dev/null +++ b/src/WinRT.Projection.Writer/Resolvers/AbiTypeShapeResolver.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Helpers; +using WindowsRuntime.ProjectionWriter.Metadata; +using WindowsRuntime.ProjectionWriter.Models; + +namespace WindowsRuntime.ProjectionWriter.Resolvers; + +/// +/// Classifies WinRT type signatures by their ABI marshalling shape (see ). +/// +/// +/// The resolver is constructed with a reference to the so it can +/// perform cross-module type resolution (e.g. resolving an enum that lives in a different +/// reference assembly than the one currently being projected). It is the single semantic +/// entry point for "what's the shape of this type at the ABI?" — emission paths consume the +/// resolver's classification rather than reaching for the per-shape predicates directly. +/// +/// The metadata cache used for cross-module type resolution. +internal sealed class AbiTypeShapeResolver(MetadataCache cache) +{ + /// + /// Gets the metadata cache used for cross-module type resolution. + /// + public MetadataCache Cache { get; } = cache; + + /// + /// Classifies as an . + /// + /// The type signature to classify. + /// The shape classification. + public AbiTypeShape Resolve(TypeSignature signature) + { + AbiTypeShapeKind kind = ClassifyShape(signature); + return new AbiTypeShape(kind, signature); + } + + /// + /// Returns whether is a blittable WinRT primitive (the C# primitive + /// types whose layout matches the WinRT ABI directly), OR a WinRT enum (whose ABI shape is its + /// underlying integer primitive). + /// + /// The type signature to classify. + /// if blittable primitive or enum; otherwise . + public bool IsBlittablePrimitive(TypeSignature signature) + => Resolve(signature).Kind is AbiTypeShapeKind.BlittablePrimitive or AbiTypeShapeKind.Enum; + + /// + /// Returns whether is a WinRT enum (marshalled as its underlying integer). + /// + /// The type signature to classify. + /// if an enum; otherwise . + public bool IsEnumType(TypeSignature signature) + => Resolve(signature).Kind == AbiTypeShapeKind.Enum; + + /// + /// Returns whether is a WinRT struct that flows across the ABI by value + /// without per-field marshalling (i.e. blittable struct, including bool/char fields). + /// Complex structs (with reference fields requiring per-field marshalling) are not included. + /// + /// The type signature to classify. + /// if a blittable struct; otherwise . + public bool IsBlittableStruct(TypeSignature signature) + => Resolve(signature).Kind == AbiTypeShapeKind.BlittableStruct; + + /// + /// Returns whether is any WinRT struct that flows across the ABI by value + /// (either a blittable struct or a complex struct that needs per-field marshalling). + /// + /// The type signature to classify. + /// if a struct; otherwise . + public bool IsAnyStruct(TypeSignature signature) + => Resolve(signature).Kind is AbiTypeShapeKind.BlittableStruct or AbiTypeShapeKind.ComplexStruct; + + /// + /// Returns whether is a WinRT struct that has at least one reference-type + /// field and therefore requires per-field marshalling via a *Marshaller class. + /// + /// The type signature to classify. + /// if a complex struct; otherwise . + public bool IsComplexStruct(TypeSignature signature) + => Resolve(signature).Kind == AbiTypeShapeKind.ComplexStruct; + + /// + /// Returns whether is a WinRT runtime class, interface, or delegate + /// (i.e. flows across the ABI as IInspectable*). + /// + /// The type signature to classify. + /// if a class / interface / delegate; otherwise . + public bool IsRuntimeClassOrInterface(TypeSignature signature) + => Resolve(signature).Kind is AbiTypeShapeKind.RuntimeClassOrInterface or AbiTypeShapeKind.Delegate; + + /// + /// Returns whether is a mapped value type that needs ABI-specific + /// marshalling (Windows.Foundation.DateTime, Windows.Foundation.TimeSpan). + /// + /// The type signature to classify. + /// if mapped; otherwise . + public bool IsMappedAbiValueType(TypeSignature signature) + => Resolve(signature).Kind == AbiTypeShapeKind.MappedAbiValueType; + + /// + /// Inner classification routine. Returns the resolved for + /// ; returns when the + /// signature does not match any known WinRT marshalling shape (typically because of an + /// unresolved cross-module reference). + /// + /// The type signature to classify. + /// The classification kind. + private AbiTypeShapeKind ClassifyShape(TypeSignature signature) + { + // Cheap top-level shape checks that don't need the cache. + if (signature.IsString()) + { + return AbiTypeShapeKind.String; + } + + if (signature.IsObject()) + { + return AbiTypeShapeKind.Object; + } + + if (signature.IsHResultException()) + { + return AbiTypeShapeKind.HResultException; + } + + if (signature.IsSystemType()) + { + return AbiTypeShapeKind.SystemType; + } + + if (signature.IsNullableT()) + { + return AbiTypeShapeKind.NullableT; + } + + if (signature is SzArrayTypeSignature) + { + return AbiTypeShapeKind.Array; + } + + if (signature.IsGenericInstance()) + { + return AbiTypeShapeKind.GenericInstance; + } + + // Cache-aware classifications. These are evaluated in the same order the writer's + // emission paths historically queried the inline AbiTypeHelpers predicates so the + // resolver returns the same shape the legacy code path would have inferred. + if (AbiTypeHelpers.IsMappedAbiValueType(signature)) + { + return AbiTypeShapeKind.MappedAbiValueType; + } + + if (AbiTypeHelpers.IsBlittablePrimitive(Cache, signature)) + { + return signature is CorLibTypeSignature ? AbiTypeShapeKind.BlittablePrimitive : AbiTypeShapeKind.Enum; + } + + if (AbiTypeHelpers.IsComplexStruct(Cache, signature)) + { + return AbiTypeShapeKind.ComplexStruct; + } + + if (AbiTypeHelpers.IsAnyStruct(Cache, signature)) + { + return AbiTypeShapeKind.BlittableStruct; + } + + if (AbiTypeHelpers.IsRuntimeClassOrInterface(Cache, signature)) + { + if (signature is TypeDefOrRefSignature td && + td.Type is TypeDefinition def && + TypeCategorization.GetCategory(def) == TypeCategory.Delegate) + { + return AbiTypeShapeKind.Delegate; + } + + return AbiTypeShapeKind.RuntimeClassOrInterface; + } + + return AbiTypeShapeKind.Unknown; + } +} diff --git a/src/WinRT.Projection.Writer/Resolvers/ParameterCategoryResolver.cs b/src/WinRT.Projection.Writer/Resolvers/ParameterCategoryResolver.cs new file mode 100644 index 0000000000..1681d09944 --- /dev/null +++ b/src/WinRT.Projection.Writer/Resolvers/ParameterCategoryResolver.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.Models; + +namespace WindowsRuntime.ProjectionWriter.Resolvers; + +/// +/// Helpers for classifying values into kinds. +/// +internal static class ParameterCategoryResolver +{ + /// + /// Returns the that describes how is + /// passed across the WinRT ABI boundary. + /// + /// The parameter to classify. + /// The classified parameter category. + public static ParameterCategory GetParamCategory(ParameterInfo 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 = p.Type.IsByRefType() || p.Parameter.ParameterType.IsByRefType(); + // 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 && p.Type.StripByRefAndCustomModifiers() is SzArrayTypeSignature; + + if (isArray || isByRefArray) + { + if (isIn) + { + return ParameterCategory.PassArray; + } + + if (isByRef && isOut) + { + return ParameterCategory.ReceiveArray; + } + + return ParameterCategory.FillArray; + } + + if (isOut) + { + return ParameterCategory.Out; + } + + if (isByRef) + { + return ParameterCategory.Ref; + } + + return ParameterCategory.In; + } +} diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Dispatching/Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Controls.Primitives/Microsoft.UI.Xaml.Controls.Primitives.GeneratorPosition.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.KeyTime.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Animation/Microsoft.UI.Xaml.Media.Animation.RepeatBehavior.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media.Media3D/Microsoft.UI.Xaml.Media.Media3D.Matrix3D.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml.Media/Microsoft.UI.Xaml.Media.Matrix.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.CornerRadius.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Duration.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs similarity index 97% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs index 585e12eace..78a1fabadb 100644 --- a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs +++ b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs @@ -32,7 +32,8 @@ public GridLength(double value, GridUnitType type) { throw new ArgumentException(SR.DirectUI_InvalidArgument, nameof(value)); } - if (type != GridUnitType.Auto && type != GridUnitType.Pixel && type != GridUnitType.Star) + + if (type is not (GridUnitType.Auto or GridUnitType.Pixel or GridUnitType.Star)) { throw new ArgumentException(SR.DirectUI_InvalidArgument, nameof(type)); } diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.Thickness.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml._SR.cs b/src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml._SR.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml._SR.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml._SR.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.Storage/WindowsRuntimeStorageExtensions.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Controls.Primitives/Windows.UI.Xaml.Controls.Primitives.GeneratorPosition.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.KeyTime.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media.Animation/Windows.UI.Xaml.Media.Animation.RepeatBehavior.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media.Media3D/Windows.UI.Xaml.Media.Media3D.Matrix3D.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml.Media/Windows.UI.Xaml.Media.Matrix.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.System.DispatcherQueueSynchronizationContext.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.CornerRadius.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Duration.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs similarity index 97% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs index e0d45bb9d1..15be3b17a7 100644 --- a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs +++ b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs @@ -32,7 +32,8 @@ public GridLength(double value, GridUnitType type) { throw new ArgumentException(SR.DirectUI_InvalidArgument, nameof(value)); } - if (type != GridUnitType.Auto && type != GridUnitType.Pixel && type != GridUnitType.Star) + + if (type is not (GridUnitType.Auto or GridUnitType.Pixel or GridUnitType.Star)) { throw new ArgumentException(SR.DirectUI_InvalidArgument, nameof(type)); } diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.Thickness.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml._SR.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml._SR.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml._SR.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml._SR.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI/Windows.UI.Color.cs b/src/WinRT.Projection.Writer/Resources/Additions/Windows.UI/Windows.UI.Color.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Additions/Windows.UI/Windows.UI.Color.cs rename to src/WinRT.Projection.Writer/Resources/Additions/Windows.UI/Windows.UI.Color.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Base/ComInteropExtensions.cs b/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs similarity index 93% rename from src/WinRT.Projection.Generator.Writer/Resources/Base/ComInteropExtensions.cs rename to src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs index 644b8c94c1..eee5707d3b 100644 --- a/src/WinRT.Projection.Generator.Writer/Resources/Base/ComInteropExtensions.cs +++ b/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs @@ -42,7 +42,9 @@ namespace Windows.ApplicationModel.DataTransfer.DragDrop.Core public static class CoreDragDropManagerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IDragDropManagerInterop. + /// + /// The cached activation factory, as IDragDropManagerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.ApplicationModel.DataTransfer.DragDrop.Core.CoreDragDropManager", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IDragDropManagerInterop); @@ -83,12 +85,16 @@ namespace Windows.Graphics.Printing public static class PrintManagerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IPrintManagerInterop. + /// + /// The cached activation factory, as IPrintManagerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.Graphics.Printing.PrintManager", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IPrintManagerInterop); - /// The accessor for __uuidof(IAsyncOperation<bool>). + /// + /// The accessor for __uuidof(IAsyncOperation<bool>). + /// [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_IID_<#CsWinRT>IAsyncOperation'1")] private static extern ref readonly Guid IID_IAsyncOperation_bool([UnsafeAccessorType("ABI.InterfaceIIDs, WinRT.Interop")] object _); #endif @@ -145,7 +151,9 @@ namespace Windows.Media public static class SystemMediaTransportControlsExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as ISystemMediaTransportControlsInterop. + /// + /// The cached activation factory, as ISystemMediaTransportControlsInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.Media.SystemMediaTransportControls", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_ISystemMediaTransportControlsInterop); @@ -184,7 +192,9 @@ namespace Windows.Media.PlayTo public static class PlayToManagerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IPlayToManagerInterop. + /// + /// The cached activation factory, as IPlayToManagerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.Media.PlayTo.PlayToManager", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IPlayToManagerInterop); @@ -242,12 +252,16 @@ namespace Windows.Security.Credentials.UI public static class UserConsentVerifierExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IUserConsentVerifierInterop. + /// + /// The cached activation factory, as IUserConsentVerifierInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.Security.Credentials.UI.UserConsentVerifier", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IUserConsentVerifierInterop); - /// The accessor for __uuidof(IAsyncOperation<UserConsentVerificationResult>). + /// + /// The accessor for __uuidof(IAsyncOperation<UserConsentVerificationResult>). + /// [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_IID_<#CsWinRT>IAsyncOperation'1<<#Windows>Windows-Security-Credentials-UI-UserConsentVerificationResult>")] private static extern ref readonly Guid IID_IAsyncOperation_UserConsentVerificationResult([UnsafeAccessorType("ABI.InterfaceIIDs, WinRT.Interop")] object _); #endif @@ -290,12 +304,16 @@ namespace Windows.Security.Authentication.Web.Core public static class WebAuthenticationCoreManagerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IWebAuthenticationCoreManagerInterop. + /// + /// The cached activation factory, as IWebAuthenticationCoreManagerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.Security.Authentication.Web.Core.WebAuthenticationCoreManager", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IWebAuthenticationCoreManagerInterop); - /// The accessor for __uuidof(IAsyncOperation<WebTokenRequestResult>). + /// + /// The accessor for __uuidof(IAsyncOperation<WebTokenRequestResult>). + /// [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_IID_<#CsWinRT>IAsyncOperation'1<<#Windows>Windows-Security-Authentication-Web-Core-WebTokenRequestResult>")] private static extern ref readonly Guid IID_IAsyncOperation_WebTokenRequestResult([UnsafeAccessorType("ABI.InterfaceIIDs, WinRT.Interop")] object _); #endif @@ -360,7 +378,9 @@ namespace Windows.UI.ApplicationSettings public static class AccountsSettingsPaneExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IAccountsSettingsPaneInterop. + /// + /// The cached activation factory, as IAccountsSettingsPaneInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.ApplicationSettings.AccountsSettingsPane", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IAccountsSettingsPaneInterop); @@ -437,7 +457,9 @@ namespace Windows.UI.Input public static class RadialControllerConfigurationExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IRadialControllerConfigurationInterop. + /// + /// The cached activation factory, as IRadialControllerConfigurationInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.Input.RadialControllerConfiguration", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IRadialControllerConfigurationInterop); @@ -473,7 +495,9 @@ public static RadialControllerConfiguration GetForWindow(nint hwnd) public static class RadialControllerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IRadialControllerInterop. + /// + /// The cached activation factory, as IRadialControllerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.Input.RadialController", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IRadialControllerInterop); @@ -512,7 +536,9 @@ namespace Windows.UI.Input.Core public static class RadialControllerIndependentInputSourceExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IRadialControllerIndependentInputSourceInterop. + /// + /// The cached activation factory, as IRadialControllerIndependentInputSourceInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.Input.Core.RadialControllerIndependentInputSource", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IRadialControllerIndependentInputSourceInterop); @@ -550,7 +576,9 @@ namespace Windows.UI.Input.Spatial public static class SpatialInteractionManagerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as ISpatialInteractionManagerInterop. + /// + /// The cached activation factory, as ISpatialInteractionManagerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.Input.Spatial.SpatialInteractionManager", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_ISpatialInteractionManagerInterop); @@ -589,7 +617,9 @@ namespace Windows.UI.ViewManagement public static class InputPaneExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IInputPaneInterop. + /// + /// The cached activation factory, as IInputPaneInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.ViewManagement.InputPane", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IInputPaneInterop); @@ -625,7 +655,9 @@ public static InputPane GetForWindow(nint appWindow) public static class UIViewSettingsExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IUIViewSettingsInterop. + /// + /// The cached activation factory, as IUIViewSettingsInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.UI.ViewManagement.UIViewSettings", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IUIViewSettingsInterop); @@ -665,7 +697,9 @@ namespace Windows.Graphics.Display public static class DisplayInformationExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IDisplayInformationStaticsInterop. + /// + /// The cached activation factory, as IDisplayInformationStaticsInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.Graphics.Display.DisplayInformation", iid: in global::ABI.InterfaceIIDs.IID_WindowsRuntime_Internal_IDisplayInformationStaticsInterop); @@ -724,7 +758,9 @@ namespace Windows.ApplicationModel.DataTransfer public static class DataTransferManagerExtensions { #if !CSWINRT_REFERENCE_PROJECTION - /// The cached activation factory, as IDataTransferManagerInterop. + /// + /// The cached activation factory, as IDataTransferManagerInterop. + /// private static readonly WindowsRuntimeObjectReference objectReference = WindowsRuntimeObjectReference.GetActivationFactory( runtimeClassName: "Windows.ApplicationModel.DataTransfer.DataTransferManager", iid: in global::ABI.WindowsRuntime.Internal.IDataTransferManagerInteropMethods.IID); @@ -779,7 +815,9 @@ namespace ABI.WindowsRuntime.Internal /// internal static class IDataTransferManagerInteropMethods { - /// The IID of IDataTransferManagerInterop (3A3DCD6C-3EAB-43DC-BCDE-45671CE800C8). + /// + /// The IID of IDataTransferManagerInterop (3A3DCD6C-3EAB-43DC-BCDE-45671CE800C8). + /// public static ref readonly Guid IID { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Base/InspectableVftbl.cs b/src/WinRT.Projection.Writer/Resources/Base/InspectableVftbl.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Base/InspectableVftbl.cs rename to src/WinRT.Projection.Writer/Resources/Base/InspectableVftbl.cs diff --git a/src/WinRT.Projection.Generator.Writer/Resources/Base/ReferenceInterfaceEntries.cs b/src/WinRT.Projection.Writer/Resources/Base/ReferenceInterfaceEntries.cs similarity index 100% rename from src/WinRT.Projection.Generator.Writer/Resources/Base/ReferenceInterfaceEntries.cs rename to src/WinRT.Projection.Writer/Resources/Base/ReferenceInterfaceEntries.cs diff --git a/src/WinRT.Projection.Generator.Writer/WinRT.Projection.Generator.Writer.csproj b/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj similarity index 51% rename from src/WinRT.Projection.Generator.Writer/WinRT.Projection.Generator.Writer.csproj rename to src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj index 7d9ab03a6c..cd4fb39a64 100644 --- a/src/WinRT.Projection.Generator.Writer/WinRT.Projection.Generator.Writer.csproj +++ b/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj @@ -1,4 +1,4 @@ - + net10.0 14.0 @@ -11,12 +11,11 @@ true - WindowsRuntime.ProjectionGenerator.Writer + WindowsRuntime.ProjectionWriter $(VersionString) @@ -29,8 +28,21 @@ 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 + + + + + + $(NoWarn);IDE0010;IDE0022;IDE0046;IDE0060;IDE0072 @@ -52,8 +64,8 @@ --> + LogicalName="WindowsRuntime.ProjectionWriter.Resources.Additions.Microsoft.UI.Xaml.Microsoft.UI.Xaml.SR.cs" /> + LogicalName="WindowsRuntime.ProjectionWriter.Resources.Additions.Windows.UI.Xaml.Windows.UI.Xaml.SR.cs" /> diff --git a/src/WinRT.Projection.Writer/Writers/IndentedTextWriter.cs b/src/WinRT.Projection.Writer/Writers/IndentedTextWriter.cs new file mode 100644 index 0000000000..751b6b6942 --- /dev/null +++ b/src/WinRT.Projection.Writer/Writers/IndentedTextWriter.cs @@ -0,0 +1,465 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Adapted from src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs (which itself +// was ported from ComputeSharp); reshaped here as a class backed by StringBuilder so it can be +// passed around freely and live as long as needed in this long-running tool. + +using System; +using System.IO; +using System.Text; + +namespace WindowsRuntime.ProjectionWriter.Writers; + +/// +/// A general-purpose helper for building C# source text with consistent indentation. +/// +/// +/// +/// This type only knows about writing indented text. WinRT-specific concerns (file headers, +/// projection namespace blocks, mode flags, metadata cache) live elsewhere -- typically as +/// extension methods. See WinRT.Interop.Generator's separation of concerns for the +/// equivalent design. +/// +/// +/// Indentation is applied per line: the first after a +/// newline (or at buffer start) prepends the current indentation string; mid-line writes do not. +/// Multiline content (passed with isMultiline: true) normalizes CRLF -> LF +/// and indents each line via the current indentation level. Empty lines never receive +/// indentation, so raw multi-line literals with blank lines do not gain trailing whitespace. +/// +/// +internal sealed class IndentedTextWriter +{ + /// + /// The default indentation (4 spaces). + /// + private const string DefaultIndentation = " "; + + /// + /// The default new line character ('\n'). + /// + private const char DefaultNewLine = '\n'; + + /// + /// The underlying buffer that text is written to. + /// + private readonly StringBuilder _buffer; + + /// + /// The current indentation string (cached for fast reuse). + /// + private string _currentIndentation; + + /// + /// Cached pre-built indentation strings indexed by indentation level. + /// + private string[] _availableIndentations; + + /// + /// Gets the current indent level (number of -equivalent units of indentation). + /// + public int CurrentIndentLevel { get; private set; } + + /// + /// Initializes a new instance of the class with an empty buffer. + /// + public IndentedTextWriter() + { + _buffer = new StringBuilder(); + CurrentIndentLevel = 0; + _currentIndentation = string.Empty; + _availableIndentations = new string[4]; + _availableIndentations[0] = string.Empty; + for (int i = 1; i < _availableIndentations.Length; i++) + { + _availableIndentations[i] = _availableIndentations[i - 1] + DefaultIndentation; + } + } + + /// + /// Increases the current indentation level by one. + /// + public void IncreaseIndent() + { + CurrentIndentLevel++; + + if (CurrentIndentLevel == _availableIndentations.Length) + { + Array.Resize(ref _availableIndentations, _availableIndentations.Length * 2); + } + + _currentIndentation = _availableIndentations[CurrentIndentLevel] + ??= _availableIndentations[CurrentIndentLevel - 1] + DefaultIndentation; + } + + /// + /// Decreases the current indentation level by one. + /// + public void DecreaseIndent() + { + CurrentIndentLevel--; + _currentIndentation = _availableIndentations[CurrentIndentLevel]; + } + + /// + /// Writes an opening brace on the current line, increases the indentation level, and returns + /// a token whose method closes the block by + /// decreasing the indentation level and writing the matching closing brace on its own line. + /// + /// A value that, when disposed, closes the open block. + public Block WriteBlock() + { + WriteLine("{"); + IncreaseIndent(); + return new Block(this); + } + + /// + /// Writes to the underlying buffer, applying current indentation + /// at the start of each new line. + /// + /// The content to write. + /// When , treats as multiline (normalizes CRLF -> LF and indents every line). + public void Write(string content, bool isMultiline = false) + { + Write(content.AsSpan(), isMultiline); + } + + /// + /// Writes to the underlying buffer, applying current indentation + /// at the start of each new line. + /// + /// The content to write. + /// When , treats as multiline (normalizes CRLF -> LF and indents every line). + public void Write(scoped ReadOnlySpan content, bool isMultiline = false) + { + if (isMultiline) + { + while (content.Length > 0) + { + int newLineIndex = content.IndexOf(DefaultNewLine); + + if (newLineIndex < 0) + { + // No newline left -- write the rest as a single line. The caller is + // responsible for emitting any trailing newline they need. + WriteRawText(content); + + break; + } + + ReadOnlySpan line = content[..newLineIndex]; + + // Strip trailing CR so output is normalized to LF regardless of source line endings. + if (line is [.. var trim, '\r']) + { + line = trim; + } + + // Skip writing empty lines (avoids trailing whitespace from leading indentation + // on what would otherwise be a blank line in the multi-line literal). + if (!line.IsEmpty) + { + WriteRawText(line); + } + + WriteLine(); + + content = content[(newLineIndex + 1)..]; + } + } + else + { + WriteRawText(content); + } + } + + /// + /// Writes to the underlying buffer if is ; otherwise does nothing. + /// + /// When , writes ; otherwise this call is a no-op. + /// The content to write. + /// When , treats as multiline. + public void WriteIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + Write(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes to the underlying buffer if is ; otherwise does nothing. + /// + /// When , writes ; otherwise this call is a no-op. + /// The content to write. + /// When , treats as multiline. + public void WriteIf(bool condition, scoped ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + } + } + + /// + /// Writes a newline to the underlying buffer. + /// + /// When , the newline is suppressed if the + /// buffer already ends with a blank line (\n\n) or with {\n. Used by callers + /// that want explicit blank-line collapse on a per-call basis. Defaults to . + public void WriteLine(bool skipIfPresent = false) + { + if (skipIfPresent && _buffer.Length > 0) + { + int j = _buffer.Length - 1; + + // Skip trailing spaces on the last line so the check ignores indentation + // that may have been emitted speculatively for an empty line. + while (j >= 0 && _buffer[j] == ' ') + { + j--; + } + + if (j >= 1 && _buffer[j] == '\n' && (_buffer[j - 1] == '\n' || _buffer[j - 1] == '{')) + { + return; + } + } + + _ = _buffer.Append(DefaultNewLine); + } + + /// + /// Writes a newline to the underlying buffer unconditionally. Equivalent to + /// WriteLine(skipIfPresent: false). + /// + public void WriteRawNewLine() + { + _ = _buffer.Append(DefaultNewLine); + } + + /// + /// Writes to the underlying buffer and appends a trailing newline. + /// + /// The content to write. + /// When , treats as multiline. + public void WriteLine(string content, bool isMultiline = false) + { + WriteLine(content.AsSpan(), isMultiline); + } + + /// + /// Writes to the underlying buffer and appends a trailing newline. + /// + /// The content to write. + /// When , treats as multiline. + public void WriteLine(scoped ReadOnlySpan content, bool isMultiline = false) + { + Write(content, isMultiline); + WriteLine(); + } + + /// + /// Writes a newline if is . + /// + /// When , writes a newline; otherwise this call is a no-op. + /// Forwarded to . + public void WriteLineIf(bool condition, bool skipIfPresent = false) + { + if (condition) + { + WriteLine(skipIfPresent); + } + } + + /// + /// Writes followed by a newline if is . + /// + /// When , writes +newline; otherwise this call is a no-op. + /// The content to write. + /// When , treats as multiline. + public void WriteLineIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + WriteLine(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes followed by a newline if is . + /// + /// When , writes +newline; otherwise this call is a no-op. + /// The content to write. + /// When , treats as multiline. + public void WriteLineIf(bool condition, scoped ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + WriteLine(); + } + } + + /// + /// Returns the current buffer contents (trimmed) and clears the buffer. + /// + /// The text accumulated so far, trimmed of leading/trailing whitespace. + public string ToStringAndClear() + { + string text = _buffer.ToString().Trim(); + _ = _buffer.Clear(); + return text; + } + + /// + /// Returns the current buffer contents without modifying the buffer. + /// + public override string ToString() => _buffer.ToString(); + + /// + /// Gets the current length (in chars) of the underlying buffer. + /// + public int Length => _buffer.Length; + + /// + /// Returns the last character written to the buffer, or '\0' if the buffer is empty. + /// + public char Back() => _buffer.Length == 0 ? '\0' : _buffer[^1]; + + /// + /// Returns the contents of a substring of the buffer (used for capture-and-restore patterns). + /// + /// The starting position. + /// The length of the substring to return. + /// The substring of the buffer at the requested position. + public string GetSubstring(int startIndex, int length) => _buffer.ToString(startIndex, length); + + /// + /// Removes a range of characters from the buffer. + /// + /// The starting position to remove. + /// The number of characters to remove. + public void Remove(int startIndex, int length) => _buffer.Remove(startIndex, length); + + /// + /// Sets the indent level back to zero (for emergency reset; rarely needed). + /// + public void ResetIndent() + { + CurrentIndentLevel = 0; + _currentIndentation = _availableIndentations[0]; + } + + /// + /// Clears the underlying buffer and resets the current indentation level back to zero. + /// Used by to recycle a returned writer + /// before handing it back out, so callers always observe a fully reset writer regardless + /// of how the previous lease left it. + /// + public void Clear() + { + _ = _buffer.Clear(); + ResetIndent(); + } + + /// + /// Flushes the current buffer to (skipping the write if the file + /// already exists with identical content), then clears the buffer. + /// + /// + /// If the destination file exists but cannot be read (e.g. due to a transient I/O failure or + /// access denial), the catch block silently falls through to a fresh write. This is the + /// intended behavior for a build tool: the worst case is an extra write of identical content + /// that the OS will then re-permit; the alternative (failing the build) would create + /// brittleness around incidental file-system noise. + /// + /// The destination file path. + public void FlushToFile(string path) + { + string content = _buffer.ToString(); + + if (File.Exists(path)) + { + try + { + if (File.ReadAllText(path) == content) + { + _ = _buffer.Clear(); + return; + } + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + // Intentional: see -- a failed read falls through to a fresh write. + } + } + + File.WriteAllText(path, content); + _ = _buffer.Clear(); + } + + /// + /// Flushes the current buffer contents to a string (without trimming) and clears the buffer. + /// + /// The full buffer contents, untrimmed. + public string FlushToString() + { + string text = _buffer.ToString(); + _ = _buffer.Clear(); + return text; + } + + /// + /// Writes raw text to the underlying buffer, prepending current indentation if positioned + /// at the start of a new line. + /// + /// The raw text to write. + private void WriteRawText(scoped ReadOnlySpan content) + { + // Skip writing indent for empty content so that empty lines never receive + // trailing whitespace from the indentation prefix. + if (content.IsEmpty) + { + return; + } + + if (_buffer.Length == 0 || _buffer[^1] == DefaultNewLine) + { + _ = _buffer.Append(_currentIndentation); + } + + _ = _buffer.Append(content); + } + + /// + /// Represents an open { ... } block that needs to be closed (via + /// ). Returned from . + /// + public struct Block : IDisposable + { + private IndentedTextWriter? _writer; + + internal Block(IndentedTextWriter writer) + { + _writer = writer; + } + + /// + /// Closes the open block by decreasing the indentation level and writing a matching + /// closing brace on its own line. + /// + public void Dispose() + { + IndentedTextWriter? writer = _writer; + _writer = null; + if (writer is not null) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + } +} diff --git a/src/WinRT.Projection.Writer/Writers/IndentedTextWriterPool.cs b/src/WinRT.Projection.Writer/Writers/IndentedTextWriterPool.cs new file mode 100644 index 0000000000..328c12f006 --- /dev/null +++ b/src/WinRT.Projection.Writer/Writers/IndentedTextWriterPool.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; + +namespace WindowsRuntime.ProjectionWriter.Writers; + +/// +/// Pool of instances reused across the projection emission +/// pipeline. The writer is the workhorse of the codebase (every projected class, interface, +/// struct, IID expression, and per-method scratch buffer instantiates one or more), so +/// recycling them across work items eliminates the per-emission +/// allocation and the four-entry _availableIndentations array allocation that would +/// otherwise happen on every call. +/// +/// +/// +/// Mirrors the TypeSignatureBuilderPool / IidHashSetPool pattern in +/// WinRT.Interop.Generator (see InteropTypeDiscovery.cs): a private static +/// backs the pool so the parallel projection-emission pipeline +/// can lease and return writers from any thread without explicit locking. +/// +/// +/// Writers are -ed on lease (in ) +/// and returned via the disposable token. The lease +/// shape is always a using block so the writer is returned to the pool on every exit +/// path (including exceptions). Pool growth is unbounded by design: under steady-state +/// parallel load the pool naturally caps at the worker-thread high-water mark. +/// +/// +internal static class IndentedTextWriterPool +{ + /// + /// The backing pool. is the right primitive for thread-local + /// take/add patterns: each thread maintains a thread-local list and only walks other + /// threads' lists on starvation, which fits the projection-writer lease pattern (lots of + /// short leases per worker, with occasional cross-thread steals). + /// + private static readonly ConcurrentBag Pool = []; + + /// + /// Leases an from the pool, wrapped in a disposable + /// token. The token returns the writer to the pool + /// when disposed, so the caller pattern is always + /// using IndentedTextWriterOwner owner = IndentedTextWriterPool.GetOrCreate(); + /// followed by owner.Writer.Write(...). + /// + /// The owning disposable token. Access the underlying writer via . + public static IndentedTextWriterOwner GetOrCreate() + { + if (Pool.TryTake(out IndentedTextWriter? writer)) + { + writer.Clear(); + return new IndentedTextWriterOwner(writer); + } + return new IndentedTextWriterOwner(new IndentedTextWriter()); + } + + /// + /// Returns to the pool for reuse. Called by + /// ; not for direct use. + /// + /// The writer to return. + internal static void Return(IndentedTextWriter writer) + { + Pool.Add(writer); + } +} + +/// +/// A disposable lease token for an obtained from +/// . The token is a +/// so it can only live on the stack: it's never accidentally captured by an async lambda or +/// stashed in a heap field, which would defeat the lifetime model. +/// +/// +/// +/// Disposal is idempotent and self-protecting: sets the inner reference +/// to after returning the writer to the pool, so a double-dispose (e.g. +/// from a buggy refactor) cannot return the same instance twice. After disposal the +/// property throws . +/// +/// +internal ref struct IndentedTextWriterOwner : IDisposable +{ + private IndentedTextWriter? _writer; + + /// + /// Initializes a new owner around . Internal: callers obtain + /// instances via . + /// + /// The leased writer. + internal IndentedTextWriterOwner(IndentedTextWriter writer) + { + _writer = writer; + } + + /// + /// Gets the leased . Throws + /// if the owner has already been disposed (i.e. was called). + /// + public readonly IndentedTextWriter Writer + { + get + { + ObjectDisposedException.ThrowIf(_writer is null, typeof(IndentedTextWriterOwner)); + return _writer; + } + } + + /// + /// Returns the leased writer to the pool and clears the inner reference so subsequent + /// accesses (or duplicate calls) cannot + /// re-pool the same instance. + /// + public void Dispose() + { + if (_writer is { } writer) + { + _writer = null; + IndentedTextWriterPool.Return(writer); + } + } +} diff --git a/src/WinRT.Projection.Writer/eng/README.md b/src/WinRT.Projection.Writer/eng/README.md new file mode 100644 index 0000000000..12e76d430b --- /dev/null +++ b/src/WinRT.Projection.Writer/eng/README.md @@ -0,0 +1,68 @@ +# validate-writer-output.ps1 + +A regression harness that catches accidental output drift in +`WinRT.Projection.Writer`. It runs the writer against a configured set of +scenarios (each described by an `.rsp` response file, the same format +`WinRT.Projection.Writer.TestRunner` accepts), captures a SHA256 manifest of +every emitted `.cs` file, and compares the result against a previously +captured baseline. + +## Usage + +```powershell +# First run: capture the baseline manifests for every .rsp scenario +.\validate-writer-output.ps1 -Mode capture + +# Subsequent runs: validate that the writer still produces byte-identical output +.\validate-writer-output.ps1 -Mode validate + +# Convenience: capture if no baseline exists yet, otherwise overwrite on drift +.\validate-writer-output.ps1 -Mode capture-and-validate +``` + +## Parameters + +| Parameter | Default | Purpose | +|---|---|---| +| `-Mode` | (required) | One of `capture`, `validate`, `capture-and-validate`. | +| `-RepoRoot` | the repo root, derived from the script's location | Override if running the script from outside the standard repo layout. | +| `-RspRoot` | `$RepoRoot\eng\rsp` | The directory containing the `.rsp` files. Each `.rsp` file becomes one scenario, named by its file stem. | +| `-Scenarios` | every `*.rsp` under `-RspRoot` | Restrict the scenario set when validating only a subset. | +| `-Configuration` | `Release` | The build configuration used to locate the TestRunner exe. | + +## Per-scenario manifest layout + +For every scenario, the script writes a `.sha256` file under +`$PSScriptRoot\baselines\.sha256` containing one line per emitted +`.cs` file: + +``` + +``` + +Drift is reported with file-by-file diffs (added / removed / changed). + +## Notes + +- The `.rsp` files are not committed alongside the script because they encode + paths into local `.winmd` metadata sources that vary between machines. Each + contributor sets up their own `.rsp` files for the scenarios they care + about, then captures a baseline against the writer state they consider + correct, and validates from there. +- The harness validates byte-for-byte equality of the emitted `.cs` files + against the captured baseline. If a refactor intentionally changes the + emitted formatting (whitespace, ordering, etc.) the contributor is expected + to recapture the baselines after manually reviewing that the change is + benign. + +## Why no xunit / unit test project? + +The writer's correctness is verified end-to-end via this harness rather than +through a parallel xunit test project, mirroring the convention used by +`WinRT.Interop.Generator` (which also has no xunit tests of its own — its +correctness is validated by integration-level tests in `src/Tests/`). +End-to-end byte-identity testing across the eight projection scenarios catches +real correctness regressions at a granularity that unit tests of helpers like +`IndentedTextWriter` would miss (e.g. interactions between brace-prepend +rules, namespace nesting, and the multi-line raw-string emission paths). + diff --git a/src/WinRT.Projection.Writer/eng/validate-writer-output.ps1 b/src/WinRT.Projection.Writer/eng/validate-writer-output.ps1 new file mode 100644 index 0000000000..87f2efcfa5 --- /dev/null +++ b/src/WinRT.Projection.Writer/eng/validate-writer-output.ps1 @@ -0,0 +1,150 @@ +# Golden-output validation harness for WinRT.Projection.Writer. +# +# Modes: +# .\validate-writer-output.ps1 -Mode capture # capture baseline manifests for the configured scenarios +# .\validate-writer-output.ps1 -Mode validate # run every scenario, compare hashes, exit non-zero on drift +# .\validate-writer-output.ps1 -Mode capture-and-validate # capture if missing; otherwise validate, overwrite on drift +# +# Per-scenario manifests are stored under: $PSScriptRoot\baselines\.sha256 +# Each scenario is described by an .rsp response file (the same format the TestRunner accepts); +# the .rsp files live under $RspRoot and select the input metadata + output directory for one +# regen scenario. The harness loads all .rsp files under $RspRoot whose name (without extension) +# matches one of the configured -Scenarios. +# +# This script is environment-portable: -RepoRoot defaults to the repo root inferred from the +# script's own location, and -RspRoot defaults to a sibling 'rsp' directory under -RepoRoot. +# Override either one when calling the script if your local layout differs. + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [ValidateSet('capture','validate','capture-and-validate')] + [string]$Mode, + [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path, + [string]$RspRoot = (Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path 'eng\rsp'), + [string[]]$Scenarios, + [string]$Configuration = 'Release' +) + +$ErrorActionPreference = 'Stop' +$baselineDir = Join-Path $PSScriptRoot 'baselines' +if (-not (Test-Path $baselineDir)) { New-Item -ItemType Directory -Path $baselineDir | Out-Null } + +# Auto-discover scenarios from the .rsp directory if none were explicitly listed. +if (-not $Scenarios) { + if (-not (Test-Path $RspRoot)) { + throw "RspRoot '$RspRoot' does not exist. Pass -RspRoot pointing at a folder of .rsp files, or -Scenarios ." + } + $Scenarios = Get-ChildItem $RspRoot -Filter '*.rsp' | Sort-Object Name | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Name) } + if (-not $Scenarios) { + throw "No .rsp files found under '$RspRoot'." + } +} + +# Locate the TestRunner exe. +$runner = "$RepoRoot\src\WinRT.Projection.Writer.TestRunner\bin\$Configuration\net10.0\WinRT.Projection.Writer.TestRunner.exe" +if (-not (Test-Path $runner)) { + Write-Host 'TestRunner exe not found; building...' -ForegroundColor Yellow + $proj = "$RepoRoot\src\WinRT.Projection.Writer.TestRunner\WinRT.Projection.Writer.TestRunner.csproj" + if (-not (Test-Path $proj)) { throw "TestRunner csproj not found at '$proj'." } + $buildLog = & dotnet build $proj -c $Configuration --nologo 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host 'TestRunner build failed:' -ForegroundColor Red + $buildLog | Select-Object -Last 25 | ForEach-Object { Write-Host $_ } + throw 'TestRunner build failed.' + } + if (-not (Test-Path $runner)) { throw "TestRunner exe still not found at '$runner' after build." } +} + +Write-Host "TestRunner: $runner" -ForegroundColor Cyan + +function Get-ManifestForScenario { + param([string]$ScenarioName) + $rsp = Join-Path $RspRoot "$ScenarioName.rsp" + if (-not (Test-Path $rsp)) { return $null } + $outDir = (((Get-Content $rsp -Raw) -split "`r?`n") | Where-Object { $_ -match '^--output-directory' } | Select-Object -First 1) -replace '^--output-directory ', '' + $outDir = $outDir.Trim() + if (-not (Test-Path $outDir)) { return $null } + $sb = [System.Text.StringBuilder]::new() + Get-ChildItem "$outDir\*.cs" | Sort-Object Name | ForEach-Object { + $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + [void]$sb.Append($hash).Append(' ').AppendLine($_.Name) + } + return $sb.ToString() +} + +function Run-Scenario { + param([string]$ScenarioName) + $rsp = Join-Path $RspRoot "$ScenarioName.rsp" + if (-not (Test-Path $rsp)) { Write-Host "SKIP: $ScenarioName ($rsp not found)" -ForegroundColor Yellow; return $false } + $output = & $runner 'rsp' $rsp 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "$ScenarioName : EXEC FAILED (exit=$LASTEXITCODE)" -ForegroundColor Red + $output | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" } + return $false + } + return $true +} + +$anyFailure = $false + +foreach ($scenario in $Scenarios) { + $rsp = Join-Path $RspRoot "$scenario.rsp" + if (-not (Test-Path $rsp)) { Write-Host "SKIP: $scenario (rsp missing)" -ForegroundColor Yellow; continue } + + $baselinePath = Join-Path $baselineDir "$scenario.sha256" + + if ($Mode -eq 'capture') { + if (-not (Run-Scenario $scenario)) { $anyFailure = $true; continue } + $manifest = Get-ManifestForScenario $scenario + if ($manifest -eq $null) { Write-Host "$scenario : no output dir after run" -ForegroundColor Red; $anyFailure = $true; continue } + Set-Content -Path $baselinePath -Value $manifest -NoNewline -Encoding utf8NoBOM + $count = ($manifest.TrimEnd("`r`n").Split("`n") | Measure-Object).Count + Write-Host ("{0,-40} CAPTURED files={1}" -f $scenario, $count) -ForegroundColor Green + } + elseif ($Mode -eq 'validate') { + if (-not (Test-Path $baselinePath)) { Write-Host "$scenario : NO BASELINE (run -Mode capture first)" -ForegroundColor Red; $anyFailure = $true; continue } + if (-not (Run-Scenario $scenario)) { $anyFailure = $true; continue } + $current = Get-ManifestForScenario $scenario + $baseline = Get-Content -Path $baselinePath -Raw + if ($current -eq $baseline) { + $count = ($current.TrimEnd("`r`n").Split("`n") | Measure-Object).Count + Write-Host ("{0,-40} OK files={1}" -f $scenario, $count) -ForegroundColor Green + } else { + Write-Host ("{0,-40} DRIFT" -f $scenario) -ForegroundColor Red + $baseLines = $baseline.Split("`n") | Where-Object { $_ } + $curLines = $current.Split("`n") | Where-Object { $_ } + $bMap = @{}; $cMap = @{} + foreach ($l in $baseLines) { $i = $l.IndexOf(' '); $bMap[$l.Substring($i+1).Trim()] = $l.Substring(0,$i) } + foreach ($l in $curLines) { $i = $l.IndexOf(' '); $cMap[$l.Substring($i+1).Trim()] = $l.Substring(0,$i) } + $allFiles = ($bMap.Keys + $cMap.Keys) | Sort-Object -Unique + $shown = 0 + foreach ($f in $allFiles) { + if ($shown -ge 25) { Write-Host " ... (more drifted files omitted)" -ForegroundColor DarkGray; break } + $bh = $bMap[$f]; $ch = $cMap[$f] + if (-not $bh) { Write-Host " + $f (added)" -ForegroundColor Green; $shown++ } + elseif (-not $ch) { Write-Host " - $f (removed)" -ForegroundColor Red; $shown++ } + elseif ($bh -ne $ch) { Write-Host " ~ $f (changed)" -ForegroundColor Yellow; $shown++ } + } + $anyFailure = $true + } + } + elseif ($Mode -eq 'capture-and-validate') { + if (-not (Run-Scenario $scenario)) { $anyFailure = $true; continue } + $manifest = Get-ManifestForScenario $scenario + if (-not (Test-Path $baselinePath)) { + Set-Content -Path $baselinePath -Value $manifest -NoNewline -Encoding utf8NoBOM + Write-Host ("{0,-40} CAPTURED (no prior baseline)" -f $scenario) -ForegroundColor Green + } else { + $baseline = Get-Content -Path $baselinePath -Raw + if ($manifest -eq $baseline) { + Write-Host ("{0,-40} OK" -f $scenario) -ForegroundColor Green + } else { + Set-Content -Path $baselinePath -Value $manifest -NoNewline -Encoding utf8NoBOM + Write-Host ("{0,-40} UPDATED (drift detected, baseline overwritten)" -f $scenario) -ForegroundColor Yellow + } + } + } +} + +if ($anyFailure) { exit 1 } else { exit 0 } diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 503dc0daad..cbc53422e0 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -427,7 +427,7 @@ - +