From 6d6f9e2ef4c630d2a02946f10eae2351c72b81e5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Jun 2026 22:50:02 -0700 Subject: [PATCH 1/5] Plumb managed component implementation assemblies into the projection writer Expose the authored component's compiled implementation assembly path(s) to the projection writer via ProjectionWriterOptions.ComponentImplementationAssemblyPaths (carried through Settings.ComponentImplementationAssemblies). The component .winmd does not carry implementation details such as the static fields backing XAML dependency properties, so the writer will consult these managed assemblies to make per-type activation-factory decisions. The projection generator host collects, in component mode, the paths of reference assemblies marked [WindowsRuntimeComponentAssembly] and forwards them through. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/ProjectionGenerator.Generate.cs | 16 ++++++++++++++-- .../Generation/Settings.cs | 8 ++++++++ src/WinRT.Projection.Writer/ProjectionWriter.cs | 1 + .../ProjectionWriterOptions.cs | 13 +++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs index b64717ab0..469a0a333 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs @@ -109,6 +109,11 @@ private static void BuildWriterOptions( List excludes = []; List winmdInputs = []; + // Paths to the managed implementation assemblies of the component(s) being projected, scanned + // by the projection writer for implementation details (e.g. the static fields backing XAML + // dependency properties) that are not present in the .winmd metadata + List componentImplementationAssemblies = []; + // Filter out .winmd files from the resolver paths string[] resolverPaths = [.. args.ReferenceAssemblyPaths .Where(p => !p.EndsWith(".winmd", StringComparison.OrdinalIgnoreCase))]; @@ -137,9 +142,15 @@ private static void BuildWriterOptions( ModuleDefinition refModule = ModuleDefinition.FromFile(refPath, resolver.ReaderParameters); - if (IsComponentAssembly(refModule) && refModule.Assembly?.Name is Utf8String name) + if (IsComponentAssembly(refModule)) { - _ = componentAssemblyNameSet.Add(name.Value); + // Keep the path so the projection writer can inspect the managed implementation + componentImplementationAssemblies.Add(refPath); + + if (refModule.Assembly?.Name is Utf8String name) + { + _ = componentAssemblyNameSet.Add(name.Value); + } } } @@ -272,6 +283,7 @@ private static void BuildWriterOptions( Include = includes, Exclude = excludes, Component = componentMode, + ComponentImplementationAssemblyPaths = componentImplementationAssemblies, MaxDegreesOfParallelism = args.MaxDegreesOfParallelism, CancellationToken = args.Token, }; diff --git a/src/WinRT.Projection.Writer/Generation/Settings.cs b/src/WinRT.Projection.Writer/Generation/Settings.cs index 47490fd35..f5c2dc29c 100644 --- a/src/WinRT.Projection.Writer/Generation/Settings.cs +++ b/src/WinRT.Projection.Writer/Generation/Settings.cs @@ -102,6 +102,14 @@ public TypeFilter AdditionFilter /// public bool Component { get; init; } + /// + /// Gets the paths to the managed implementation assemblies of the authored Windows Runtime + /// component(s) being projected. Used in component mode to inspect implementation details that + /// are absent from the input .winmd metadata (e.g. the static fields backing XAML + /// dependency properties). May be empty when those assemblies are not available. + /// + public HashSet ComponentImplementationAssemblies { get; } = []; + /// /// Gets or sets a value indicating whether [ExclusiveTo] interfaces are emitted as public rather than internal. /// diff --git a/src/WinRT.Projection.Writer/ProjectionWriter.cs b/src/WinRT.Projection.Writer/ProjectionWriter.cs index 943dafdee..e07785cd8 100644 --- a/src/WinRT.Projection.Writer/ProjectionWriter.cs +++ b/src/WinRT.Projection.Writer/ProjectionWriter.cs @@ -57,6 +57,7 @@ public static void Run(ProjectionWriterOptions options) settings.Include.UnionWith(options.Include); settings.Exclude.UnionWith(options.Exclude); settings.AdditionExclude.UnionWith(options.AdditionExclude); + settings.ComponentImplementationAssemblies.UnionWith(options.ComponentImplementationAssemblyPaths); settings.MakeReadOnly(); diff --git a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs index 6c9bd4e77..03a56008c 100644 --- a/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs +++ b/src/WinRT.Projection.Writer/ProjectionWriterOptions.cs @@ -46,6 +46,19 @@ public sealed class ProjectionWriterOptions /// public bool Component { get; init; } + /// + /// Optional paths to the managed implementation assemblies (.dll) of the authored + /// Windows Runtime component(s) being projected. Only meaningful in mode. + /// + /// + /// The input .winmd metadata does not carry implementation details such as the + /// static fields backing XAML dependency properties, so the writer consults these + /// managed assemblies to decide whether each generated activation factory needs to force the + /// authored type's class constructor to run before activation. When the list is empty (e.g. the + /// managed assembly is not available yet), the writer conservatively keeps that constructor. + /// + public IReadOnlyList ComponentImplementationAssemblyPaths { get; init; } = []; + /// /// Make exclusive-to interfaces public in the projection (default is internal). /// From 0ba5a194b1bc22b5953e6f12dcfff50b8d50f71b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Jun 2026 23:07:22 -0700 Subject: [PATCH 2/5] Skip activation factory static constructor when not needed The generated 'ServerActivationFactory' classes always emitted a static constructor calling RuntimeHelpers.RunClassConstructor(typeof(Type).TypeHandle). That is only required when the authored type registers XAML dependency properties (declared as static DependencyProperty fields), so that they are registered before the type is first activated. For every other authored type it is pure overhead. The factory now emits the static constructor only when the authored type, or an authored base type, declares a static field of type DependencyProperty (Windows.UI.Xaml.DependencyProperty for UWP XAML, otherwise Microsoft.UI.Xaml.DependencyProperty). This is determined by scanning the managed component implementation assemblies (the .winmd does not carry these fields) via the new ComponentImplementationMetadata helper; types not found there conservatively keep the constructor. The conditional emission goes through a WriteStaticConstructor callback so the factory body remains a single interpolated template either way. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/SignatureComparerExtensions.cs | 29 ++++ .../Factories/ComponentFactory.cs | 37 ++++- .../ProjectionGenerator.Component.cs | 23 ++- .../ProjectionGenerator.Namespace.cs | 2 +- .../Generation/ProjectionGenerator.cs | 5 +- .../Generation/ProjectionGeneratorRunState.cs | 13 +- .../ComponentImplementationMetadata.cs | 142 ++++++++++++++++++ .../References/WellKnownNamespaces.cs | 10 ++ .../References/WellKnownTypeNames.cs | 7 + .../WinRT.Projection.Writer.csproj | 5 +- 10 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs create mode 100644 src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs diff --git a/src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs new file mode 100644 index 000000000..1909e1e27 --- /dev/null +++ b/src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.ProjectionWriter; + +/// +/// Extensions for . +/// +internal static class SignatureComparerExtensions +{ +#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed + /// + /// Backing field for the extension property. + /// + private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); +#pragma warning restore IDE0052 + + extension(SignatureComparer) + { + /// + /// Gets a shared, version-agnostic , suitable for comparing + /// AsmResolver entities (e.g. as the + /// for a of ). + /// + public static SignatureComparer IgnoreVersion => IgnoreVersion; + } +} diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs index 4c559d9c3..d1818ba80 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -50,7 +50,15 @@ public static void AddMetadataTypeEntry(ProjectionEmitContext context, TypeDefin /// /// Writes the per-runtime-class server-activation-factory type for component mode. /// - public static void WriteFactoryClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) + /// The writer to emit the factory class to. + /// The active projection emit context. + /// The activatable runtime class to emit a factory for. + /// + /// Whether to emit the static constructor that forces the authored type's class constructor to + /// run before activation. Only needed when the type registers dependency properties (see + /// ). + /// + public static void WriteFactoryClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool emitStaticConstructor) { (string typeNs, string typeName) = type.Names(); string projectedTypeName = TypedefNameWriter.BuildGlobalQualifiedName(typeNs, typeName); @@ -90,6 +98,27 @@ void WriteActivateInstanceBody(IndentedTextWriter writer) } } + // Writes the static constructor that forces the projected type's class constructor to run + // before activation, so any dependency properties it registers (as static fields) are set + // up in time. Types that don't register any don't need it, so the callback emits nothing + // for them and the factory omits the constructor entirely, keeping the factory body a + // single interpolated template in either case + void WriteStaticConstructor(IndentedTextWriter writer) + { + if (!emitStaticConstructor) + { + return; + } + + writer.WriteLine(); + writer.WriteLine(isMultiline: true, $$""" + static {{factoryTypeName}}() + { + global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof({{projectedTypeName}}).TypeHandle); + } + """); + } + // Helper wrapper to write additional methods void WriteAdditionalActivationFactoryMethods(IndentedTextWriter writer) { @@ -101,12 +130,8 @@ void WriteAdditionalActivationFactoryMethods(IndentedTextWriter writer) internal sealed class {{factoryTypeName}} : {{WriteBaseInterfaceList}} { private static readonly {{factoryTypeName}} _factory = new(); + {{WriteStaticConstructor}} - static {{factoryTypeName}}() - { - global::System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof({{projectedTypeName}}).TypeHandle); - } - public static unsafe void* Make() { return global::WindowsRuntime.InteropServices.Marshalling.WindowsRuntimeInterfaceMarshaller diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs index 68b1260e1..1bdd917b1 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; using WindowsRuntime.ProjectionWriter.Factories; using WindowsRuntime.ProjectionWriter.Metadata; using WindowsRuntime.ProjectionWriter.Writers; @@ -24,18 +25,25 @@ internal sealed partial class ProjectionGenerator /// /// 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 ) + /// RequiringStaticConstructor -- the subset of activatable classes whose activation factory must force the authored type's class constructor to run (because the type, or an authored base type, registers a dependency property) /// /// - private (HashSet ComponentActivatable, Dictionary> ByModule) DiscoverComponentActivatableTypes() + private (HashSet ComponentActivatable, Dictionary> ByModule, HashSet RequiringStaticConstructor) DiscoverComponentActivatableTypes() { - HashSet componentActivatable = []; + HashSet componentActivatable = new(SignatureComparer.IgnoreVersion); Dictionary> componentByModule = []; + HashSet requiringStaticConstructor = new(SignatureComparer.IgnoreVersion); if (!_settings.Component) { - return (componentActivatable, componentByModule); + return (componentActivatable, componentByModule, requiringStaticConstructor); } + // The .winmd metadata does not carry the static fields backing XAML dependency properties, + // so consult the authored component's managed implementation assemblies to decide which + // activation factories actually need to force the authored type's class constructor to run + ComponentImplementationMetadata implementationMetadata = ComponentImplementationMetadata.Load(_settings.ComponentImplementationAssemblies); + foreach ((_, NamespaceMembers members) in _cache.Namespaces) { foreach (TypeDefinition type in members.Classes) @@ -53,16 +61,21 @@ internal sealed partial class ProjectionGenerator if (!componentByModule.TryGetValue(moduleName, out HashSet? set)) { - set = []; + set = new(SignatureComparer.IgnoreVersion); componentByModule[moduleName] = set; } _ = set.Add(type); + + if (implementationMetadata.RequiresStaticConstructor(type.FullName)) + { + _ = requiringStaticConstructor.Add(type); + } } } } - return (componentActivatable, componentByModule); + return (componentActivatable, componentByModule, requiringStaticConstructor); } /// diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs index 5275e3681..adbc28ed7 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs @@ -137,7 +137,7 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe if (_settings.Component && componentActivatable.Contains(type)) { - ComponentFactory.WriteFactoryClass(writer, context, type); + ComponentFactory.WriteFactoryClass(writer, context, type, state.ComponentActivatableRequiringStaticConstructor.Contains(type)); } } else if (kind is TypeKind.Delegate or TypeKind.Enum or TypeKind.Interface) diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs index 3b07176cc..0bacdc4a4 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs @@ -55,11 +55,12 @@ public void Run() { HashSet componentActivatable; Dictionary> componentByModule; + HashSet componentActivatableRequiringStaticConstructor; // Phase 1: discover the activatable runtime classes (component mode only). try { - (componentActivatable, componentByModule) = DiscoverComponentActivatableTypes(); + (componentActivatable, componentByModule, componentActivatableRequiringStaticConstructor) = DiscoverComponentActivatableTypes(); } catch (Exception e) when (!e.IsWellKnown) { @@ -68,7 +69,7 @@ public void Run() _token.ThrowIfCancellationRequested(); - ProjectionGeneratorRunState state = new(componentActivatable, componentByModule); + ProjectionGeneratorRunState state = new(componentActivatable, componentByModule, componentActivatableRequiringStaticConstructor); // Phase 2: parallel emission. All file writes happen below; wrap the whole emission // pipeline in a single try/catch so any unexpected failure surfaces as an diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs index 915781e84..697bb960d 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs @@ -29,6 +29,14 @@ internal sealed class ProjectionGeneratorRunState /// public Dictionary> ComponentByModule { get; } + /// + /// Gets the subset of whose activation factory must force the + /// authored type's class constructor to run before activation (because the type, or an authored + /// base type, registers a dependency property). Read-only after construction; safe for concurrent + /// reads from any work item. + /// + public HashSet ComponentActivatableRequiringStaticConstructor { get; } + /// /// Gets the (projected-type-name -> default-interface-name) map populated by namespace /// work items via . @@ -61,12 +69,15 @@ internal sealed class ProjectionGeneratorRunState /// /// The flat set of activatable runtime classes. /// The activatable classes grouped by source module name. + /// The subset of activatable classes whose activation factory must force the authored type's class constructor to run. public ProjectionGeneratorRunState( HashSet componentActivatable, - Dictionary> componentByModule) + Dictionary> componentByModule, + HashSet componentActivatableRequiringStaticConstructor) { ComponentActivatable = componentActivatable; ComponentByModule = componentByModule; + ComponentActivatableRequiringStaticConstructor = componentActivatableRequiringStaticConstructor; } /// diff --git a/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs b/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs new file mode 100644 index 000000000..6a264053c --- /dev/null +++ b/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.ProjectionWriter.References; + +namespace WindowsRuntime.ProjectionWriter.Metadata; + +/// +/// Indexes implementation details of the authored Windows Runtime component(s) being projected, +/// read from their managed implementation assemblies (.dll). These details are not present +/// in the .winmd metadata, so they have to be read from the compiled assemblies directly. +/// +/// +/// The only detail currently tracked is whether an authored type registers any XAML dependency +/// properties (modelled as static fields of type DependencyProperty), which drives +/// whether the generated activation factory needs to force the authored type's class constructor +/// to run before the type is activated. +/// +internal sealed class ComponentImplementationMetadata +{ + /// + /// Per-type record: whether the type itself declares a static DependencyProperty + /// field, and the full name of its base type (used to walk authored base types). + /// + private readonly record struct TypeRecord(bool DeclaresDependencyProperty, string? BaseTypeFullName); + + /// The authored types indexed by full name. + private readonly Dictionary _typesByFullName; + + private ComponentImplementationMetadata(Dictionary typesByFullName) + { + _typesByFullName = typesByFullName; + } + + /// + /// Loads the metadata for the given managed implementation assemblies. When the input is empty, + /// the returned instance is empty and conservatively + /// reports that every type needs its class constructor. + /// + /// The managed implementation assembly paths to scan. + /// The loaded metadata. + public static ComponentImplementationMetadata Load(IEnumerable assemblyPaths) + { + // Type full names are compared ordinally, which is the default string equality used here + Dictionary typesByFullName = []; + + foreach (string assemblyPath in assemblyPaths) + { + ModuleDefinition module = ModuleDefinition.FromFile(assemblyPath); + + foreach (TypeDefinition type in module.GetAllTypes()) + { + string fullName = type.FullName; + + // Skip the '' pseudo-type and only index the first definition for a given + // full name (multiple component assemblies should never define the same type) + if (string.IsNullOrEmpty(fullName) || type.Name?.Value is "") + { + continue; + } + + _ = typesByFullName.TryAdd(fullName, new TypeRecord(DeclaresDependencyProperty(type), type.BaseType?.FullName)); + } + } + + return new ComponentImplementationMetadata(typesByFullName); + } + + /// + /// Determines whether the activation factory for the authored type identified by + /// must force the type's class constructor to run before + /// activation, i.e. whether the type (or any authored base type) registers a dependency property. + /// + /// The full name of the authored type. + /// if the static constructor is required; otherwise, . + /// + /// If the type is not found among the scanned implementation assemblies, this returns + /// : we cannot prove the constructor is unnecessary, so it is kept (this + /// preserves the previous behavior whenever no implementation assemblies are available). The + /// hierarchy walk stops at the first base type outside the scanned assemblies (e.g. a framework + /// base type), because framework dependency properties are registered by the framework itself. + /// + public bool RequiresStaticConstructor(string typeFullName) + { + if (!_typesByFullName.ContainsKey(typeFullName)) + { + return true; + } + + HashSet visited = []; + string? current = typeFullName; + + while (current is not null && visited.Add(current) && _typesByFullName.TryGetValue(current, out TypeRecord record)) + { + if (record.DeclaresDependencyProperty) + { + return true; + } + + current = record.BaseTypeFullName; + } + + return false; + } + + /// + /// Returns whether declares any static field whose type is the + /// XAML DependencyProperty (in either Microsoft.UI.Xaml or Windows.UI.Xaml). + /// + /// The type to inspect. + /// if the type declares such a field; otherwise, . + private static bool DeclaresDependencyProperty(TypeDefinition type) + { + foreach (FieldDefinition field in type.Fields) + { + if (!field.IsStatic) + { + continue; + } + + TypeSignature? fieldType = field.Signature?.FieldType; + + if (fieldType is null) + { + continue; + } + + (string ns, string name) = fieldType.Names(); + + if (name == WellKnownTypeNames.DependencyProperty && + (ns == WellKnownNamespaces.MicrosoftUIXaml || ns == WellKnownNamespaces.WindowsUIXaml)) + { + return true; + } + } + + return false; + } +} diff --git a/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs b/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs index 333546ce6..7c8e5fca9 100644 --- a/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs +++ b/src/WinRT.Projection.Writer/References/WellKnownNamespaces.cs @@ -37,4 +37,14 @@ internal static class WellKnownNamespaces /// The Windows.UI.Xaml.Interop namespace. /// public const string WindowsUIXamlInterop = "Windows.UI.Xaml.Interop"; + + /// + /// The Windows.UI.Xaml namespace (UWP XAML, where DependencyProperty lives in that mode). + /// + public const string WindowsUIXaml = "Windows.UI.Xaml"; + + /// + /// The Microsoft.UI.Xaml namespace (WinUI, where DependencyProperty lives in that mode). + /// + public const string MicrosoftUIXaml = "Microsoft.UI.Xaml"; } diff --git a/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs b/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs index 5e1435571..12a84dae7 100644 --- a/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs +++ b/src/WinRT.Projection.Writer/References/WellKnownTypeNames.cs @@ -52,4 +52,11 @@ internal static class WellKnownTypeNames /// The Windows SDK XAML TypeName struct (the WinMD source for ). /// public const string TypeName = "TypeName"; + + /// + /// The XAML DependencyProperty type (in either Microsoft.UI.Xaml for WinUI or + /// Windows.UI.Xaml for UWP XAML). Authored types register dependency properties as + /// static fields of this type. + /// + public const string DependencyProperty = "DependencyProperty"; } diff --git a/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj b/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj index 3c100fcc6..472523484 100644 --- a/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj +++ b/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj @@ -30,7 +30,7 @@ + - $(NoWarn);IDE0010;IDE0022;IDE0046;IDE0060;IDE0072 + $(NoWarn);IDE0010;IDE0022;IDE0028;IDE0046;IDE0060;IDE0072 From 14cf3ef13e08f98d86bcaaa2a47fffd980cd7075 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Jun 2026 00:24:31 -0700 Subject: [PATCH 3/5] Move SignatureComparer.IgnoreVersion to the shared generator core The version-agnostic SignatureComparer used as the IEqualityComparer for AsmResolver entity collections lived only in the interop generator. The projection writer now needs it too (for its component activatable-type sets), so promote it to the shared WinRT.Generator.Core library as a single SignatureComparerExtensions.IgnoreVersion and have both tools consume it: - Add SignatureComparerExtensions (IgnoreVersion) to WinRT.Generator.Core and make Core's internals visible to WinRT.Projection.Writer. - Reference Core from the projection writer and drop the writer's local copy. - Remove the interop generator's duplicate IgnoreVersion (keeping its tuple comparers) and resolve it from Core instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/SignatureComparerExtensions.cs | 4 +--- src/WinRT.Generator.Core/WinRT.Generator.Core.csproj | 3 +++ .../InteropTypeDefinitionBuilder.IObservableMap2.cs | 3 ++- ...nteropTypeDefinitionBuilder.IObservableVector1.cs | 3 ++- .../Discovery/InteropTypeDiscovery.Generics.cs | 3 ++- .../Discovery/InteropTypeDiscovery.cs | 3 ++- .../Extensions/ComExtensions.cs | 3 ++- .../Extensions/IHasCustomAttributeExtensions.cs | 3 ++- .../Extensions/ModuleDefinitionExtensions.cs | 3 ++- .../Extensions/SignatureComparerExtensions.cs | 12 ------------ .../Extensions/TypeDefinitionExtensions.cs | 3 ++- .../Extensions/WindowsRuntimeExtensions.cs | 3 ++- ...thodDefinitionFactory.IReadOnlyDictionary2Impl.cs | 3 ++- ...eropMethodDefinitionFactory.IReadOnlyList1Impl.cs | 3 ++- .../Generation/InteropGenerator.Discover.cs | 3 ++- .../Generation/InteropGeneratorDiscoveryState.cs | 3 ++- .../Generation/InteropGeneratorEmitState.cs | 3 ++- .../Helpers/TypeExclusions.cs | 1 + .../Helpers/WindowsRuntimeTypeAnalyzer.cs | 1 + .../Models/TypeSignatureEquatableSet.Builder.cs | 3 ++- .../Models/TypeSignatureEquatableSet.cs | 3 ++- .../References/WellKnownInterfaceIIDs.cs | 1 + .../Resolvers/InteropImplTypeResolver.cs | 1 + .../Resolvers/InteropInterfaceEntriesResolver.cs | 1 + .../InteropMethodRewriter.ManagedParameter.cs | 3 ++- .../Rewriters/InteropMethodRewriter.ManagedValue.cs | 3 ++- .../InteropMethodRewriter.NativeParameter.cs | 3 ++- .../Rewriters/InteropMethodRewriter.ReturnValue.cs | 3 ++- .../Generation/ProjectionGenerator.Component.cs | 1 + .../WinRT.Projection.Writer.csproj | 4 ++++ 30 files changed, 54 insertions(+), 35 deletions(-) rename src/{WinRT.Projection.Writer => WinRT.Generator.Core}/Extensions/SignatureComparerExtensions.cs (86%) diff --git a/src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Generator.Core/Extensions/SignatureComparerExtensions.cs similarity index 86% rename from src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs rename to src/WinRT.Generator.Core/Extensions/SignatureComparerExtensions.cs index 1909e1e27..9ecb901c0 100644 --- a/src/WinRT.Projection.Writer/Extensions/SignatureComparerExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/SignatureComparerExtensions.cs @@ -3,19 +3,17 @@ using AsmResolver.DotNet.Signatures; -namespace WindowsRuntime.ProjectionWriter; +namespace WindowsRuntime.Generator; /// /// Extensions for . /// internal static class SignatureComparerExtensions { -#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed /// /// Backing field for the extension property. /// private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); -#pragma warning restore IDE0052 extension(SignatureComparer) { diff --git a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj index 5e0d083a0..ecbb167af 100644 --- a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj +++ b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj @@ -40,5 +40,8 @@ + + + diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableMap2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableMap2.cs index ea4708ee8..0654706c6 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableMap2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableMap2.cs @@ -5,6 +5,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Factories; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.Helpers; @@ -533,4 +534,4 @@ public static void ImplType( implType.Properties.Add(mapChangedTableProperty); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableVector1.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableVector1.cs index 90707e9a9..7f325ee78 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableVector1.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IObservableVector1.cs @@ -5,6 +5,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Factories; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.Helpers; @@ -521,4 +522,4 @@ public static void ImplType( emitState.TrackTypeDefinition(implType, vectorType, "Impl"); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs index 6eed89eeb..28f6bcf97 100644 --- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs +++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs @@ -3,6 +3,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.Helpers; @@ -636,4 +637,4 @@ private static void TryTrackManagedGenericTypeInstance( interopReferences: interopReferences, module: module); } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs index c044ab3e1..7bc0a9767 100644 --- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs +++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs @@ -7,6 +7,7 @@ using System.Linq; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.Helpers; @@ -562,4 +563,4 @@ private static bool TryAddExposedInterfaceType( return true; } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/ComExtensions.cs b/src/WinRT.Interop.Generator/Extensions/ComExtensions.cs index 20719c0ae..7df003065 100644 --- a/src/WinRT.Interop.Generator/Extensions/ComExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/ComExtensions.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.References; namespace WindowsRuntime.InteropGenerator; @@ -72,4 +73,4 @@ public bool TryGetInterfaceInformationType(InteropReferences interopReferences, return true; } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/IHasCustomAttributeExtensions.cs b/src/WinRT.Interop.Generator/Extensions/IHasCustomAttributeExtensions.cs index b12417026..132090edf 100644 --- a/src/WinRT.Interop.Generator/Extensions/IHasCustomAttributeExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/IHasCustomAttributeExtensions.cs @@ -5,6 +5,7 @@ using AsmResolver; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; namespace WindowsRuntime.InteropGenerator; @@ -100,4 +101,4 @@ public static bool HasCustomAttribute(this IHasCustomAttribute member, ITypeDesc { return TryGetCustomAttribute(member, attributeType, out _); } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/ModuleDefinitionExtensions.cs b/src/WinRT.Interop.Generator/Extensions/ModuleDefinitionExtensions.cs index c50cc44e7..4e6e1ba2e 100644 --- a/src/WinRT.Interop.Generator/Extensions/ModuleDefinitionExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/ModuleDefinitionExtensions.cs @@ -9,6 +9,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Helpers; using WindowsRuntime.InteropGenerator.Visitors; @@ -304,4 +305,4 @@ public static IEnumerable OrderByFullyQualifiedName(this IEnum { return modules.Order(ModuleDefinitionComparer.Instance); } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Interop.Generator/Extensions/SignatureComparerExtensions.cs index a0b6ac98e..6b9cf3b1e 100644 --- a/src/WinRT.Interop.Generator/Extensions/SignatureComparerExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/SignatureComparerExtensions.cs @@ -12,20 +12,8 @@ namespace WindowsRuntime.InteropGenerator; /// internal static class SignatureComparerExtensions { -#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed - /// - /// Backing field for . - /// - private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); -#pragma warning restore IDE0052 - extension(SignatureComparer comparer) { - /// - /// An immutable default instance of , with . - /// - public static SignatureComparer IgnoreVersion => IgnoreVersion; - /// /// Creates an instance for a pair of values. /// diff --git a/src/WinRT.Interop.Generator/Extensions/TypeDefinitionExtensions.cs b/src/WinRT.Interop.Generator/Extensions/TypeDefinitionExtensions.cs index 888c334b3..cb0849aef 100644 --- a/src/WinRT.Interop.Generator/Extensions/TypeDefinitionExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/TypeDefinitionExtensions.cs @@ -8,6 +8,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; #pragma warning disable IDE0046 @@ -344,4 +345,4 @@ public IEnumerable EnumerateGenericInstanceInterfa } } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs b/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs index ce70b8a1a..581fdb3fa 100644 --- a/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs @@ -8,6 +8,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.References; #pragma warning disable IDE0046 @@ -1186,4 +1187,4 @@ file static class WellKnownMetadataNames /// The "WindowsRuntimeComponentAssemblyAttribute" text. /// public static readonly Utf8String WindowsRuntimeComponentAssemblyAttribute = "WindowsRuntimeComponentAssemblyAttribute"u8; -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs index 27c0f0fe4..9f5262fc3 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -6,6 +6,7 @@ using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; using static AsmResolver.PE.DotNet.Cil.CilOpCodes; @@ -512,4 +513,4 @@ public static MethodDefinition Split( return splitMethod; } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs index 8abfa8e72..1844bc4c0 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs @@ -6,6 +6,7 @@ using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; using static AsmResolver.PE.DotNet.Cil.CilOpCodes; @@ -493,4 +494,4 @@ _ when elementType.IsTypeOfException(interopReferences) => interopReferences.IRe return getManyImplMethod; } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs index 42760ba3c..54196bf64 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs @@ -10,6 +10,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE; +using WindowsRuntime.Generator; using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Extensions; using WindowsRuntime.Generator.References; @@ -618,4 +619,4 @@ private static string[] GetAssembliesToProcess(InteropGeneratorArgs args) return [.. assembliesToProcess]; } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs index 979bba7ef..7b747a0c0 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Models; @@ -572,4 +573,4 @@ private void ThrowIfReadOnly() throw WellKnownInteropExceptions.DiscoveryStateChangeAfterMakeReadOnlyError(); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorEmitState.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorEmitState.cs index 265b99f1e..ab0719156 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorEmitState.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGeneratorEmitState.cs @@ -8,6 +8,7 @@ using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Models; @@ -394,4 +395,4 @@ private void ThrowIfReadOnly() throw WellKnownInteropExceptions.EmitStateChangeAfterMakeReadOnlyError(); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Helpers/TypeExclusions.cs b/src/WinRT.Interop.Generator/Helpers/TypeExclusions.cs index 74b5ab799..2356b13fe 100644 --- a/src/WinRT.Interop.Generator/Helpers/TypeExclusions.cs +++ b/src/WinRT.Interop.Generator/Helpers/TypeExclusions.cs @@ -4,6 +4,7 @@ using System; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.References; namespace WindowsRuntime.InteropGenerator.Helpers; diff --git a/src/WinRT.Interop.Generator/Helpers/WindowsRuntimeTypeAnalyzer.cs b/src/WinRT.Interop.Generator/Helpers/WindowsRuntimeTypeAnalyzer.cs index 7831c0771..85b31842c 100644 --- a/src/WinRT.Interop.Generator/Helpers/WindowsRuntimeTypeAnalyzer.cs +++ b/src/WinRT.Interop.Generator/Helpers/WindowsRuntimeTypeAnalyzer.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.References; namespace WindowsRuntime.InteropGenerator.Helpers; diff --git a/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.Builder.cs b/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.Builder.cs index 6f33602c2..199d2b2ec 100644 --- a/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.Builder.cs +++ b/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.Builder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; namespace WindowsRuntime.InteropGenerator.Models; @@ -122,4 +123,4 @@ public TypeSignatureEquatableSet MoveToEquatableSet() return new(set); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.cs b/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.cs index 6620f2705..f051e8c39 100644 --- a/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.cs +++ b/src/WinRT.Interop.Generator/Models/TypeSignatureEquatableSet.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Helpers; namespace WindowsRuntime.InteropGenerator.Models; @@ -220,4 +221,4 @@ IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs index fafda0e0f..a6384152c 100644 --- a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs +++ b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Factories; diff --git a/src/WinRT.Interop.Generator/Resolvers/InteropImplTypeResolver.cs b/src/WinRT.Interop.Generator/Resolvers/InteropImplTypeResolver.cs index 1e6dd2966..f635942b8 100644 --- a/src/WinRT.Interop.Generator/Resolvers/InteropImplTypeResolver.cs +++ b/src/WinRT.Interop.Generator/Resolvers/InteropImplTypeResolver.cs @@ -5,6 +5,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Factories; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; diff --git a/src/WinRT.Interop.Generator/Resolvers/InteropInterfaceEntriesResolver.cs b/src/WinRT.Interop.Generator/Resolvers/InteropInterfaceEntriesResolver.cs index 0de717e4f..6c2e6a6a8 100644 --- a/src/WinRT.Interop.Generator/Resolvers/InteropInterfaceEntriesResolver.cs +++ b/src/WinRT.Interop.Generator/Resolvers/InteropInterfaceEntriesResolver.cs @@ -7,6 +7,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.Models; using WindowsRuntime.InteropGenerator.References; diff --git a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedParameter.cs b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedParameter.cs index c61d54484..b547b1d2b 100644 --- a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedParameter.cs +++ b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedParameter.cs @@ -6,6 +6,7 @@ using AsmResolver.DotNet.Collections; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; @@ -116,4 +117,4 @@ public static void RewriteMethod( } } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedValue.cs b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedValue.cs index fd61a5c9f..f7a69c98e 100644 --- a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedValue.cs +++ b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ManagedValue.cs @@ -5,6 +5,7 @@ using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; @@ -99,4 +100,4 @@ public static void RewriteMethod( } } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.NativeParameter.cs b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.NativeParameter.cs index ad3a7d7c4..0185da8c6 100644 --- a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.NativeParameter.cs +++ b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.NativeParameter.cs @@ -7,6 +7,7 @@ using AsmResolver.DotNet.Collections; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; @@ -425,4 +426,4 @@ private static void RewriteBodyForTypeOfType( }); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ReturnValue.cs b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ReturnValue.cs index ae4462938..02eed83a6 100644 --- a/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ReturnValue.cs +++ b/src/WinRT.Interop.Generator/Rewriters/InteropMethodRewriter.ReturnValue.cs @@ -5,6 +5,7 @@ using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; +using WindowsRuntime.Generator; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Generation; using WindowsRuntime.InteropGenerator.References; @@ -221,4 +222,4 @@ private static void RewriteBody( }); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs index 1bdd917b1..4f24565f0 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs @@ -5,6 +5,7 @@ using System.IO; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; using WindowsRuntime.ProjectionWriter.Factories; using WindowsRuntime.ProjectionWriter.Metadata; using WindowsRuntime.ProjectionWriter.Writers; diff --git a/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj b/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj index 472523484..34eb111fc 100644 --- a/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj +++ b/src/WinRT.Projection.Writer/WinRT.Projection.Writer.csproj @@ -50,6 +50,10 @@ + + + + From 6625607ed275281e6522c251d12026a9b0972b9a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Jun 2026 00:25:36 -0700 Subject: [PATCH 4/5] Make component dependency-property analysis lazy ComponentImplementationMetadata.Load previously computed, for every type in the component's managed assemblies, whether it declared a dependency property. That work is only needed for the handful of activatable types actually projected, so make it lazy: - ComponentImplementationMetadata is now a thin resolver that just indexes the managed assembly types by full name and resolves them on demand. - A new ComponentStaticConstructorAnalyzer computes whether a type (or an authored base type) registers a dependency property, resolving and walking base types only when queried and memoizing the result per visited type so shared hierarchies are traversed once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Factories/ComponentFactory.cs | 2 +- .../ProjectionGenerator.Component.cs | 3 +- .../ComponentImplementationMetadata.cs | 115 ++++------------ .../ComponentStaticConstructorAnalyzer.cs | 124 ++++++++++++++++++ 4 files changed, 151 insertions(+), 93 deletions(-) create mode 100644 src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs index d1818ba80..0995bf90d 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -56,7 +56,7 @@ public static void AddMetadataTypeEntry(ProjectionEmitContext context, TypeDefin /// /// Whether to emit the static constructor that forces the authored type's class constructor to /// run before activation. Only needed when the type registers dependency properties (see - /// ). + /// ). /// public static void WriteFactoryClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool emitStaticConstructor) { diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs index 4f24565f0..130d38bad 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs @@ -44,6 +44,7 @@ internal sealed partial class ProjectionGenerator // so consult the authored component's managed implementation assemblies to decide which // activation factories actually need to force the authored type's class constructor to run ComponentImplementationMetadata implementationMetadata = ComponentImplementationMetadata.Load(_settings.ComponentImplementationAssemblies); + ComponentStaticConstructorAnalyzer staticConstructorAnalyzer = new(implementationMetadata); foreach ((_, NamespaceMembers members) in _cache.Namespaces) { @@ -68,7 +69,7 @@ internal sealed partial class ProjectionGenerator _ = set.Add(type); - if (implementationMetadata.RequiresStaticConstructor(type.FullName)) + if (staticConstructorAnalyzer.RequiresStaticConstructor(type.FullName)) { _ = requiringStaticConstructor.Add(type); } diff --git a/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs b/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs index 6a264053c..e4c049c71 100644 --- a/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs +++ b/src/WinRT.Projection.Writer/Metadata/ComponentImplementationMetadata.cs @@ -3,49 +3,41 @@ using System.Collections.Generic; using AsmResolver.DotNet; -using AsmResolver.DotNet.Signatures; -using WindowsRuntime.ProjectionWriter.References; namespace WindowsRuntime.ProjectionWriter.Metadata; /// -/// Indexes implementation details of the authored Windows Runtime component(s) being projected, -/// read from their managed implementation assemblies (.dll). These details are not present -/// in the .winmd metadata, so they have to be read from the compiled assemblies directly. +/// Resolves authored Windows Runtime types from the managed implementation assemblies (.dll) +/// of the component(s) being projected, by full name. The managed assemblies carry implementation +/// details (such as the static fields backing XAML dependency properties) that are not +/// present in the .winmd metadata, so consumers resolve the managed type here and inspect it +/// directly. /// /// -/// The only detail currently tracked is whether an authored type registers any XAML dependency -/// properties (modelled as static fields of type DependencyProperty), which drives -/// whether the generated activation factory needs to force the authored type's class constructor -/// to run before the type is activated. +/// Loading only builds a name index over the assemblies' types. No per-type analysis is performed +/// up front: callers resolve just the types they care about (see +/// ), keeping the cost proportional to what is used. /// internal sealed class ComponentImplementationMetadata { - /// - /// Per-type record: whether the type itself declares a static DependencyProperty - /// field, and the full name of its base type (used to walk authored base types). - /// - private readonly record struct TypeRecord(bool DeclaresDependencyProperty, string? BaseTypeFullName); + /// The authored types indexed by full name (ordinal, the default string equality). + private readonly Dictionary _typesByFullName; - /// The authored types indexed by full name. - private readonly Dictionary _typesByFullName; - - private ComponentImplementationMetadata(Dictionary typesByFullName) + private ComponentImplementationMetadata(Dictionary typesByFullName) { _typesByFullName = typesByFullName; } /// - /// Loads the metadata for the given managed implementation assemblies. When the input is empty, - /// the returned instance is empty and conservatively - /// reports that every type needs its class constructor. + /// Loads the managed implementation assemblies and indexes their types by full name. When the + /// input is empty, the returned instance resolves nothing and always + /// returns . /// - /// The managed implementation assembly paths to scan. + /// The managed implementation assembly paths to load. /// The loaded metadata. public static ComponentImplementationMetadata Load(IEnumerable assemblyPaths) { - // Type full names are compared ordinally, which is the default string equality used here - Dictionary typesByFullName = []; + Dictionary typesByFullName = []; foreach (string assemblyPath in assemblyPaths) { @@ -62,7 +54,7 @@ public static ComponentImplementationMetadata Load(IEnumerable assemblyP continue; } - _ = typesByFullName.TryAdd(fullName, new TypeRecord(DeclaresDependencyProperty(type), type.BaseType?.FullName)); + _ = typesByFullName.TryAdd(fullName, type); } } @@ -70,73 +62,14 @@ public static ComponentImplementationMetadata Load(IEnumerable assemblyP } /// - /// Determines whether the activation factory for the authored type identified by - /// must force the type's class constructor to run before - /// activation, i.e. whether the type (or any authored base type) registers a dependency property. + /// Resolves the authored type with the given full name from the loaded implementation + /// assemblies, or if no such type is present (e.g. a framework type, or + /// when no implementation assemblies were loaded). /// - /// The full name of the authored type. - /// if the static constructor is required; otherwise, . - /// - /// If the type is not found among the scanned implementation assemblies, this returns - /// : we cannot prove the constructor is unnecessary, so it is kept (this - /// preserves the previous behavior whenever no implementation assemblies are available). The - /// hierarchy walk stops at the first base type outside the scanned assemblies (e.g. a framework - /// base type), because framework dependency properties are registered by the framework itself. - /// - public bool RequiresStaticConstructor(string typeFullName) + /// The full name of the type to resolve. + /// The resolved , or . + public TypeDefinition? Resolve(string fullName) { - if (!_typesByFullName.ContainsKey(typeFullName)) - { - return true; - } - - HashSet visited = []; - string? current = typeFullName; - - while (current is not null && visited.Add(current) && _typesByFullName.TryGetValue(current, out TypeRecord record)) - { - if (record.DeclaresDependencyProperty) - { - return true; - } - - current = record.BaseTypeFullName; - } - - return false; - } - - /// - /// Returns whether declares any static field whose type is the - /// XAML DependencyProperty (in either Microsoft.UI.Xaml or Windows.UI.Xaml). - /// - /// The type to inspect. - /// if the type declares such a field; otherwise, . - private static bool DeclaresDependencyProperty(TypeDefinition type) - { - foreach (FieldDefinition field in type.Fields) - { - if (!field.IsStatic) - { - continue; - } - - TypeSignature? fieldType = field.Signature?.FieldType; - - if (fieldType is null) - { - continue; - } - - (string ns, string name) = fieldType.Names(); - - if (name == WellKnownTypeNames.DependencyProperty && - (ns == WellKnownNamespaces.MicrosoftUIXaml || ns == WellKnownNamespaces.WindowsUIXaml)) - { - return true; - } - } - - return false; + return _typesByFullName.GetValueOrDefault(fullName); } } diff --git a/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs b/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs new file mode 100644 index 000000000..02ec06157 --- /dev/null +++ b/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator; +using WindowsRuntime.ProjectionWriter.References; + +namespace WindowsRuntime.ProjectionWriter.Metadata; + +/// +/// Decides whether the activation factory generated for an authored Windows Runtime class must +/// force the class constructor to run before activation, i.e. whether the class (or an authored +/// base class) registers any XAML dependency properties. +/// +/// +/// Dependency properties are registered in static fields of type DependencyProperty, +/// so they only exist in the managed implementation assemblies, not in the .winmd metadata. +/// The analyzer resolves each queried type lazily via +/// and walks its base chain on demand, memoizing the result per visited type so that shared +/// hierarchies (e.g. several controls deriving from a common authored base) are traversed once. +/// +internal sealed class ComponentStaticConstructorAnalyzer +{ + /// The resolver used to look up authored types (and their base types) by full name. + private readonly ComponentImplementationMetadata _metadata; + + /// + /// Memoizes, per visited type, whether it requires the class constructor to run. Keyed with the + /// version-agnostic , the standard comparer for AsmResolver entities. + /// + private readonly Dictionary _cache = new(SignatureComparer.IgnoreVersion); + + /// + /// Creates a new over the given metadata. + /// + /// The resolver for the component's managed implementation types. + public ComponentStaticConstructorAnalyzer(ComponentImplementationMetadata metadata) + { + _metadata = metadata; + } + + /// + /// Determines whether the activation factory for the authored type identified by + /// must force the type's class constructor to run before + /// activation. + /// + /// The full name of the authored type. + /// if the static constructor is required; otherwise, . + /// + /// If the type cannot be resolved from the scanned implementation assemblies, this returns + /// : we cannot prove the constructor is unnecessary, so it is kept (this + /// also covers the case where no implementation assemblies are available). + /// + public bool RequiresStaticConstructor(string typeFullName) + { + TypeDefinition? type = _metadata.Resolve(typeFullName); + + return type is null || RequiresStaticConstructor(type); + } + + /// + /// Memoized core: whether (or an authored base type) registers a + /// dependency property. + /// + /// The resolved authored type to inspect. + /// if the static constructor is required; otherwise, . + private bool RequiresStaticConstructor(TypeDefinition type) + { + if (_cache.TryGetValue(type, out bool cached)) + { + return cached; + } + + // The type itself registers a dependency property, or an authored base type does. The base + // walk stops at the first type outside the scanned assemblies (e.g. a framework base type), + // because framework dependency properties are registered by the framework itself. Inheritance + // can't cycle, so a plain recursion (memoized below) always terminates. + bool result = + DeclaresDependencyProperty(type) || + (type.BaseType is { } baseType && + _metadata.Resolve(baseType.FullName) is { } baseDefinition && + RequiresStaticConstructor(baseDefinition)); + + _cache[type] = result; + + return result; + } + + /// + /// Returns whether declares any static field whose type is the + /// XAML DependencyProperty (in either Microsoft.UI.Xaml or Windows.UI.Xaml). + /// + /// The type to inspect. + /// if the type declares such a field; otherwise, . + private static bool DeclaresDependencyProperty(TypeDefinition type) + { + foreach (FieldDefinition field in type.Fields) + { + if (!field.IsStatic) + { + continue; + } + + TypeSignature? fieldType = field.Signature?.FieldType; + + if (fieldType is null) + { + continue; + } + + (string ns, string name) = fieldType.Names(); + + if (name == WellKnownTypeNames.DependencyProperty && + (ns == WellKnownNamespaces.MicrosoftUIXaml || ns == WellKnownNamespaces.WindowsUIXaml)) + { + return true; + } + } + + return false; + } +} From 5a5afbc8d0d5888c618951930fbdd15ff09d0042 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Jun 2026 11:21:19 -0700 Subject: [PATCH 5/5] Compute the activation-factory static constructor requirement on the fly WriteFactoryClass took an 'emitStaticConstructor' bool that the caller pre-computed into a separate set during discovery. Since the method already has the type and the emit context, let the context carry the component's loaded implementation assemblies and have the factory query them directly: - ProjectionEmitContext now exposes the shared ComponentStaticConstructorAnalyzer, created once on the ProjectionGenerator and reused by every emit context (so its memoization is shared). The analyzer's cache is now a ConcurrentDictionary since it is queried from the parallel emission work items (AsmResolver reads are thread-safe). - WriteFactoryClass drops the bool parameter and computes the requirement from the context. - DiscoverComponentActivatableTypes no longer builds the requiring-static-constructor set, and ProjectionGeneratorRunState no longer carries it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Factories/ComponentFactory.cs | 12 ++++++------ .../Generation/ProjectionEmitContext.cs | 15 ++++++++++++++- .../ProjectionGenerator.Component.cs | 19 +++---------------- .../ProjectionGenerator.GeneratedIids.cs | 2 +- .../ProjectionGenerator.Namespace.cs | 4 ++-- .../Generation/ProjectionGenerator.cs | 14 +++++++++++--- .../Generation/ProjectionGeneratorRunState.cs | 13 +------------ .../ComponentStaticConstructorAnalyzer.cs | 11 +++++++---- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs index 0995bf90d..c2e29a11b 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -53,17 +53,17 @@ public static void AddMetadataTypeEntry(ProjectionEmitContext context, TypeDefin /// The writer to emit the factory class to. /// The active projection emit context. /// The activatable runtime class to emit a factory for. - /// - /// Whether to emit the static constructor that forces the authored type's class constructor to - /// run before activation. Only needed when the type registers dependency properties (see - /// ). - /// - public static void WriteFactoryClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool emitStaticConstructor) + public static void WriteFactoryClass(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type) { (string typeNs, string typeName) = type.Names(); string projectedTypeName = TypedefNameWriter.BuildGlobalQualifiedName(typeNs, typeName); string factoryTypeName = $"{IdentifierEscaping.StripBackticks(typeName)}ServerActivationFactory"; + // The static constructor that forces the authored type's class constructor to run before + // activation is only needed when the type registers dependency properties, so consult the + // component's managed implementation assemblies (the .winmd doesn't carry those fields). + bool emitStaticConstructor = context.StaticConstructorAnalyzer.RequiresStaticConstructor(type.FullName); + // Writes the set of interfaces implemented by the factory class ('IActivationFactory' is always included) void WriteBaseInterfaceList(IndentedTextWriter writer) { diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs b/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs index 1be2c4aa9..405cc1ce0 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionEmitContext.cs @@ -14,7 +14,12 @@ namespace WindowsRuntime.ProjectionWriter.Generation; /// 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) +/// The analyzer over the component's managed implementation assemblies (component mode). +internal sealed class ProjectionEmitContext( + Settings settings, + MetadataCache cache, + string currentNamespace, + ComponentStaticConstructorAnalyzer staticConstructorAnalyzer) { /// /// Gets the active projection settings. @@ -53,6 +58,14 @@ internal sealed class ProjectionEmitContext(Settings settings, MetadataCache cac /// public AbiTypeKindResolver AbiTypeKindResolver { get; } = new AbiTypeKindResolver(cache); + /// + /// Gets the analyzer over the component's managed implementation assemblies (component mode). + /// It exposes implementation details that are absent from the .winmd metadata (e.g. the + /// static fields backing XAML dependency properties), computed on demand as types are + /// emitted. The same instance is shared by every emit context, so its memoization is reused. + /// + public ComponentStaticConstructorAnalyzer StaticConstructorAnalyzer { get; } = staticConstructorAnalyzer; + /// /// Gets a value indicating whether platform-attribute computation should suppress platforms /// that are less than or equal to . Used to apply class-scope platform diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs index 130d38bad..39e0d8f93 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Component.cs @@ -26,26 +26,18 @@ internal sealed partial class ProjectionGenerator /// /// 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 ) - /// RequiringStaticConstructor -- the subset of activatable classes whose activation factory must force the authored type's class constructor to run (because the type, or an authored base type, registers a dependency property) /// /// - private (HashSet ComponentActivatable, Dictionary> ByModule, HashSet RequiringStaticConstructor) DiscoverComponentActivatableTypes() + private (HashSet ComponentActivatable, Dictionary> ByModule) DiscoverComponentActivatableTypes() { HashSet componentActivatable = new(SignatureComparer.IgnoreVersion); Dictionary> componentByModule = []; - HashSet requiringStaticConstructor = new(SignatureComparer.IgnoreVersion); if (!_settings.Component) { - return (componentActivatable, componentByModule, requiringStaticConstructor); + return (componentActivatable, componentByModule); } - // The .winmd metadata does not carry the static fields backing XAML dependency properties, - // so consult the authored component's managed implementation assemblies to decide which - // activation factories actually need to force the authored type's class constructor to run - ComponentImplementationMetadata implementationMetadata = ComponentImplementationMetadata.Load(_settings.ComponentImplementationAssemblies); - ComponentStaticConstructorAnalyzer staticConstructorAnalyzer = new(implementationMetadata); - foreach ((_, NamespaceMembers members) in _cache.Namespaces) { foreach (TypeDefinition type in members.Classes) @@ -68,16 +60,11 @@ internal sealed partial class ProjectionGenerator } _ = set.Add(type); - - if (staticConstructorAnalyzer.RequiresStaticConstructor(type.FullName)) - { - _ = requiringStaticConstructor.Add(type); - } } } } - return (componentActivatable, componentByModule, requiringStaticConstructor); + return (componentActivatable, componentByModule); } /// diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs index 62fd1b734..1af69abb2 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs @@ -62,7 +62,7 @@ internal void WriteGeneratedInterfaceIidsFile() bool iidWritten = false; HashSet interfacesFromClassesEmitted = []; - ProjectionEmitContext guidContext = new(_settings, _cache, "ABI"); + ProjectionEmitContext guidContext = new(_settings, _cache, "ABI", _staticConstructorAnalyzer); using IndentedTextWriterOwner guidIndentedOwner = IndentedTextWriterPool.GetOrCreate(); IndentedTextWriter guidIndented = guidIndentedOwner.Writer; IidExpressionGenerator.WriteInterfaceIidsBegin(guidIndented); diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs index adbc28ed7..70d19b536 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs @@ -27,7 +27,7 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe ConcurrentBag> exclusiveToInterfaceEntries = state.ExclusiveToInterfaceEntries; ConcurrentDictionary authoredTypeNameToMetadataMap = state.AuthoredTypeNameToMetadataMap; HashSet componentActivatable = state.ComponentActivatable; - ProjectionEmitContext context = new(_settings, _cache, ns); + ProjectionEmitContext context = new(_settings, _cache, ns, _staticConstructorAnalyzer); using IndentedTextWriterOwner writerOwner = IndentedTextWriterPool.GetOrCreate(); IndentedTextWriter writer = writerOwner.Writer; @@ -137,7 +137,7 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe if (_settings.Component && componentActivatable.Contains(type)) { - ComponentFactory.WriteFactoryClass(writer, context, type, state.ComponentActivatableRequiringStaticConstructor.Contains(type)); + ComponentFactory.WriteFactoryClass(writer, context, type); } } else if (kind is TypeKind.Delegate or TypeKind.Enum or TypeKind.Interface) diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs index 0bacdc4a4..5f5d8a438 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.cs @@ -48,6 +48,15 @@ internal sealed partial class ProjectionGenerator(Settings settings, MetadataCac private readonly MetadataCache _cache = cache; private readonly CancellationToken _token = token; + /// + /// Analyzes the component's managed implementation assemblies to decide which activation + /// factories must force the authored type's class constructor to run. Created once and shared + /// by every emit context, so its memoization cache is reused across namespaces. Empty (and + /// unused) outside component mode, where no implementation assemblies are provided. + /// + private readonly ComponentStaticConstructorAnalyzer _staticConstructorAnalyzer = + new(ComponentImplementationMetadata.Load(settings.ComponentImplementationAssemblies)); + /// /// Runs the projection-generation pipeline end-to-end. /// @@ -55,12 +64,11 @@ public void Run() { HashSet componentActivatable; Dictionary> componentByModule; - HashSet componentActivatableRequiringStaticConstructor; // Phase 1: discover the activatable runtime classes (component mode only). try { - (componentActivatable, componentByModule, componentActivatableRequiringStaticConstructor) = DiscoverComponentActivatableTypes(); + (componentActivatable, componentByModule) = DiscoverComponentActivatableTypes(); } catch (Exception e) when (!e.IsWellKnown) { @@ -69,7 +77,7 @@ public void Run() _token.ThrowIfCancellationRequested(); - ProjectionGeneratorRunState state = new(componentActivatable, componentByModule, componentActivatableRequiringStaticConstructor); + ProjectionGeneratorRunState state = new(componentActivatable, componentByModule); // Phase 2: parallel emission. All file writes happen below; wrap the whole emission // pipeline in a single try/catch so any unexpected failure surfaces as an diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs index 697bb960d..915781e84 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGeneratorRunState.cs @@ -29,14 +29,6 @@ internal sealed class ProjectionGeneratorRunState /// public Dictionary> ComponentByModule { get; } - /// - /// Gets the subset of whose activation factory must force the - /// authored type's class constructor to run before activation (because the type, or an authored - /// base type, registers a dependency property). Read-only after construction; safe for concurrent - /// reads from any work item. - /// - public HashSet ComponentActivatableRequiringStaticConstructor { get; } - /// /// Gets the (projected-type-name -> default-interface-name) map populated by namespace /// work items via . @@ -69,15 +61,12 @@ internal sealed class ProjectionGeneratorRunState /// /// The flat set of activatable runtime classes. /// The activatable classes grouped by source module name. - /// The subset of activatable classes whose activation factory must force the authored type's class constructor to run. public ProjectionGeneratorRunState( HashSet componentActivatable, - Dictionary> componentByModule, - HashSet componentActivatableRequiringStaticConstructor) + Dictionary> componentByModule) { ComponentActivatable = componentActivatable; ComponentByModule = componentByModule; - ComponentActivatableRequiringStaticConstructor = componentActivatableRequiringStaticConstructor; } /// diff --git a/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs b/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs index 02ec06157..851092253 100644 --- a/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs +++ b/src/WinRT.Projection.Writer/Metadata/ComponentStaticConstructorAnalyzer.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; +using System.Collections.Concurrent; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using WindowsRuntime.Generator; @@ -19,7 +19,9 @@ namespace WindowsRuntime.ProjectionWriter.Metadata; /// so they only exist in the managed implementation assemblies, not in the .winmd metadata. /// The analyzer resolves each queried type lazily via /// and walks its base chain on demand, memoizing the result per visited type so that shared -/// hierarchies (e.g. several controls deriving from a common authored base) are traversed once. +/// hierarchies (e.g. several controls deriving from a common authored base) are traversed once. A +/// single instance is shared by all per-namespace emit contexts and queried from the parallel +/// emission work items, so the memoization cache is concurrent. /// internal sealed class ComponentStaticConstructorAnalyzer { @@ -27,10 +29,11 @@ internal sealed class ComponentStaticConstructorAnalyzer private readonly ComponentImplementationMetadata _metadata; /// - /// Memoizes, per visited type, whether it requires the class constructor to run. Keyed with the + /// Memoizes, per visited type, whether it requires the class constructor to run. Concurrent + /// because the analyzer is queried from the parallel emission work items. Keyed with the /// version-agnostic , the standard comparer for AsmResolver entities. /// - private readonly Dictionary _cache = new(SignatureComparer.IgnoreVersion); + private readonly ConcurrentDictionary _cache = new(SignatureComparer.IgnoreVersion); /// /// Creates a new over the given metadata.