From 47c14d814a4954a9bf4b3c9d9a781856ce52623e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 13:37:29 -0800 Subject: [PATCH 01/10] Replace ConditionalWeakTable with ConcurrentDictionary Replace the per-Type backing tables from ConditionalWeakTable to ConcurrentDictionary in DynamicInterfaceCastableImplementationInfo, WindowsRuntimeMarshallingInfo, and WindowsRuntimeMetadataInfo. Also add a using System.Collections.Concurrent directive where needed. Note: this changes lifetime semantics (ConcurrentDictionary holds strong references), which may affect object garbage-collection behavior compared to ConditionalWeakTable. --- .../TypeMapInfo/DynamicInterfaceCastableImplementationInfo.cs | 3 ++- .../TypeMapInfo/WindowsRuntimeMarshallingInfo.cs | 2 +- .../InteropServices/TypeMapInfo/WindowsRuntimeMetadataInfo.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/DynamicInterfaceCastableImplementationInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/DynamicInterfaceCastableImplementationInfo.cs index 0e7001101..aaec47730 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/DynamicInterfaceCastableImplementationInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/DynamicInterfaceCastableImplementationInfo.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -25,7 +26,7 @@ internal sealed class DynamicInterfaceCastableImplementationInfo /// /// The table of marshalling info for all types that can participate in marshalling. /// - private static readonly ConditionalWeakTable TypeToImplementationInfoTable = []; + private static readonly ConcurrentDictionary TypeToImplementationInfoTable = []; /// /// Cached creation factory for . diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs index 5bedf6606..7d9cc2f79 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs @@ -56,7 +56,7 @@ internal sealed class WindowsRuntimeMarshallingInfo /// This will only have non values for types needing special marshalling. Types which are meant to /// be marshalled as opaque IInspectable objects will have no associated values, and should be handled separately. /// - private static readonly ConditionalWeakTable TypeToMarshallingInfoTable = []; + private static readonly ConcurrentDictionary TypeToMarshallingInfoTable = []; /// /// Cached creation factory for . diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMetadataInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMetadataInfo.cs index b8f13152f..f9f2e444f 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMetadataInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMetadataInfo.cs @@ -36,7 +36,7 @@ internal sealed class WindowsRuntimeMetadataInfo /// /// This will only have non values for types needing special metadata handling. /// - private static readonly ConditionalWeakTable TypeToMetadataInfoTable = []; + private static readonly ConcurrentDictionary TypeToMetadataInfoTable = []; /// /// Cached creation factory for . From 52b4e1c3adf8d3b8e81bb01f186d6efa594978a6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 15:19:58 -0800 Subject: [PATCH 02/10] Refactor Type marshalling and add caches Introduce zero-allocation marshalling helpers and caches for System.Type handling. Adds ManagedTypeReference and TransientTypeReference types, a ManagedTypeReferenceEqualityComparer, and a TypeNameCache (two ConcurrentDictionaries optimized for UI-thread access) to enable alternate-key lookups and avoid string allocations. Split out UncachedTypeMarshaller with FromTransientTypeReference/ToManagedTypeReference to centralize non-cached logic, and update TypeMarshaller to use the cache paths and alternate lookups. Adjust Convert/Box/Unbox/Dispose flows accordingly. Remove the NoMetadataTypeInfo cache and make its constructor public so trimmed/metadata-missing types are created on demand. --- src/WinRT.Runtime2/ABI/System/Type.cs | 305 ++++++++++++++++++-------- 1 file changed, 208 insertions(+), 97 deletions(-) diff --git a/src/WinRT.Runtime2/ABI/System/Type.cs b/src/WinRT.Runtime2/ABI/System/Type.cs index 6ea224173..b211a18c5 100644 --- a/src/WinRT.Runtime2/ABI/System/Type.cs +++ b/src/WinRT.Runtime2/ABI/System/Type.cs @@ -101,13 +101,200 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR { ArgumentNullException.ThrowIfNull(value); + ManagedTypeReference typeReference = TypeNameCache.TypeToTypeNameMap.GetOrAdd(value, UncachedTypeMarshaller.ToManagedTypeReference); + + reference = new TypeReference { Name = typeReference.Name, Kind = typeReference.Kind }; + } + + /// + /// Converts an unmanaged to a managed . + /// + /// The unmanaged value. + /// The managed value + public static global::System.Type? ConvertToManaged(Type value) + { + ReadOnlySpan typeName = HStringMarshaller.ConvertToManagedUnsafe(value.Name); + + // Get the alternate lookup from the cache first and use it to try to retrieve a cached 'Type' + // instance. This allows us to avoid materializing the 'string' if we have a cache hit here. + if (TypeNameCache.TypeNameToTypeMap.GetAlternateLookup().TryGetValue( + key: new TransientTypeReference(typeName, value.Kind), + value: out global::System.Type? result)) + { + return result; + } + + // We didn't get a cached value, so we just manually marshal the value here. Note that we + // can't just use a 'GetOrAdd' overload here like above, because there isn't one available + // that supports alternate lookups. But we don't want to give up on avoiding this allocation. + result = UncachedTypeMarshaller.FromTransientTypeReference(new TransientTypeReference(typeName, value.Kind)); + + // Try to add the value to the cache. If another thread already added it, we just ignore the result. + // Marshalled 'Type' instances are guaranteed to have a 1:1 mapping, so we can still use the local + // value that we just produced, instead of having to do another lookup to get it from the cache. + _ = TypeNameCache.TypeNameToTypeMap.TryAdd(new ManagedTypeReference(typeName.ToString(), value.Kind), result); + + return result; + } + + /// + public static WindowsRuntimeObjectReferenceValue BoxToUnmanaged(global::System.Type? value) + { + return value is null ? default : new((void*)WindowsRuntimeComWrappers.Default.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.None, in WellKnownWindowsInterfaceIIDs.IID_IReferenceOfType)); + } + + /// + public static global::System.Type? UnboxToManaged(void* value) + { + Type? abi = WindowsRuntimeValueTypeMarshaller.UnboxToManaged(value); + + return abi.HasValue ? ConvertToManaged(abi.GetValueOrDefault()) : null; + } + + /// + /// Disposes resources associated with an unmanaged value. + /// + /// The unmanaged value to dispose. + public static void Dispose(Type value) + { + HStringMarshaller.Free(value.Name); + } +} + +/// +/// Represents a reference to a value, for fast marshalling to native. +/// +file readonly struct ManagedTypeReference +{ + /// + public readonly string Name; + + /// + public readonly TypeKind Kind; + + /// + /// Creates a new value with the specified parameters. + /// + /// + /// + public ManagedTypeReference(string name, TypeKind kind) + { + Name = name; + Kind = kind; + } +} + +/// +/// Represents a transient reference to a value, to avoid allocations. +/// +file readonly ref struct TransientTypeReference +{ + /// + public readonly ReadOnlySpan Name; + + /// + public readonly TypeKind Kind; + + /// + /// Creates a new value with the specified parameters. + /// + /// + /// + public TransientTypeReference(ReadOnlySpan name, TypeKind kind) + { + Name = name; + Kind = kind; + } +} + +/// +/// A custom for to support zero-allocation lookups. +/// +file sealed class ManagedTypeReferenceEqualityComparer : + IEqualityComparer, + IAlternateEqualityComparer +{ + /// + /// The singleton instance. + /// + public static readonly ManagedTypeReferenceEqualityComparer Instance = new(); + + /// + public ManagedTypeReference Create(TransientTypeReference alternate) + { + return new(alternate.Name.ToString(), alternate.Kind); + } + + /// + public bool Equals(ManagedTypeReference x, ManagedTypeReference y) + { + return x.Kind == y.Kind && string.Equals(x.Name, y.Name, StringComparison.Ordinal); + } + + /// + public bool Equals(TransientTypeReference alternate, ManagedTypeReference other) + { + return alternate.Kind == other.Kind && alternate.Name.SequenceEqual(other.Name); + } + + /// + public int GetHashCode(ManagedTypeReference obj) + { + return HashCode.Combine( + value1: string.GetHashCode(obj.Name), + value2: obj.Kind); + } + + /// + public int GetHashCode(TransientTypeReference alternate) + { + return HashCode.Combine( + value1: string.GetHashCode(alternate.Name), + value2: alternate.Kind); + } +} + +/// +/// Cached maps to speedup marshalling. +/// +file static class TypeNameCache +{ + /// + /// The cache of type names to instances. + /// + /// + /// This cache is mostly only used by XAML, meaning it should pretty much always be accessed from the UI thread. + /// Because of this, we can set the concurrency level to just '1', to reduce the memory use from this dictionary. + /// + public static readonly ConcurrentDictionary TypeNameToTypeMap = new( + concurrencyLevel: 1, + capacity: 32, + comparer: ManagedTypeReferenceEqualityComparer.Instance); + + /// + /// The cache of instances to type name values. + /// + /// + public static readonly ConcurrentDictionary TypeToTypeNameMap = new(concurrencyLevel: 1, capacity: 32); +} + +/// +/// Marshaller for using no cache. +/// +file static class UncachedTypeMarshaller +{ + /// + /// Converts a to a value. + /// + /// The value. + /// The value. + public static ManagedTypeReference ToManagedTypeReference(global::System.Type value) + { // Special case for 'NoMetadataTypeInfo' instances, which can only be obtained // from previous calls to 'ConvertToManaged' for types that had been trimmed. if (value is NoMetadataTypeInfo noMetadataTypeInfo) { - reference = new TypeReference { Name = noMetadataTypeInfo.FullName, Kind = TypeKind.Metadata }; - - return; + return new(noMetadataTypeInfo.FullName, TypeKind.Metadata); } // We need special handling for 'Nullable' values. If we have one, we want to use the underlying type @@ -127,9 +314,7 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR // Additionally, this path isn't taken if we have a nullable value type, which avoids the lookup too. if (!value.IsGenericType && value.IsDefined(typeof(WindowsRuntimeMetadataAttribute))) { - reference = new TypeReference { Name = value.FullName, Kind = TypeKind.Metadata }; - - return; + return new(value.FullName!, TypeKind.Metadata); } // Use the metadata info lookup first to handle custom-mapped interface types. These would not have a proxy @@ -137,26 +322,20 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR // being projected types from there. So we handle them here first to get the right metadata type name. if (WindowsRuntimeMetadataInfo.TryGetInfo(value, out WindowsRuntimeMetadataInfo? metadataInfo)) { - reference = new TypeReference { Name = metadataInfo.GetMetadataTypeName(), Kind = TypeKind.Metadata }; - - return; + return new(metadataInfo.GetMetadataTypeName(), TypeKind.Metadata); } } // Special case 'Exception' types, since we also need to handle all derived types (e.g. user-defined) if (value.IsAssignableTo(typeof(global::System.Exception))) { - reference = new TypeReference { Name = "Windows.Foundation.HResult", Kind = TypeKind.Metadata }; - - return; + return new("Windows.Foundation.HResult", TypeKind.Metadata); } // Special case 'Type' as well, for the same reason (e.g. 'typeof(Foo)' would return a 'RuntimeType' instance) if (value.IsAssignableTo(typeof(global::System.Type))) { - reference = new TypeReference { Name = "Windows.UI.Xaml.Interop.TypeName", Kind = TypeKind.Metadata }; - - return; + return new("Windows.UI.Xaml.Interop.TypeName", TypeKind.Metadata); } global::System.Type typeOrUnderlyingType = nullableUnderlyingType ?? value; @@ -181,9 +360,7 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR // This will also handle generic delegate types, which will also use '[WindowsRuntimeMetadataTypeName]'. if (marshallingInfo.TryGetMetadataTypeName(out string? metadataTypeName)) { - reference = new TypeReference { Name = metadataTypeName, Kind = kind }; - - return; + return new(metadataTypeName, kind); } // If the type is 'KeyValuePair<,>', we are guaranteed to have a runtime class name on the proxy type. @@ -194,9 +371,7 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR typeOrUnderlyingType.IsGenericType && typeOrUnderlyingType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) { - reference = new TypeReference { Name = marshallingInfo.GetRuntimeClassName(), Kind = kind }; - - return; + return new(marshallingInfo.GetRuntimeClassName(), kind); } // If we don't have a metadata type name, check if we have a value type or a delegate type. @@ -209,9 +384,7 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR if (typeOrUnderlyingType.IsValueType || typeOrUnderlyingType.IsAssignableTo(typeof(Delegate))) { - reference = new TypeReference { Name = typeOrUnderlyingType.FullName, Kind = kind }; - - return; + return new(typeOrUnderlyingType.FullName!, kind); } } @@ -230,16 +403,12 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR // cases such as constructed 'Nullable' types, which will report their boxed type name. if (marshallingInfo.TryGetRuntimeClassName(out string? runtimeClassName)) { - reference = new TypeReference { Name = runtimeClassName, Kind = kind }; - - return; + return new(runtimeClassName, kind); } // Otherwise, use the type name directly. This will handle all remaining cases, such as projected // runtime classes and interface types. For all of those, the projected type name will be correct. - reference = new TypeReference { Name = typeOrUnderlyingType.FullName, Kind = kind }; - - return; + return new(typeOrUnderlyingType.FullName!, kind); } // For primitive types, we always report 'TypeKind.Primitive'. This means that some @@ -250,26 +419,24 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR // custom types, which they would be otherwise, since they don't have marshalling info. if (value.IsPrimitive) { - reference = new TypeReference { Name = value.FullName, Kind = TypeKind.Primitive }; - - return; + return new(value.FullName!, TypeKind.Primitive); } CustomType: // All other cases are treated as custom types (e.g. user-defined types) - reference = new TypeReference { Name = value.AssemblyQualifiedName, Kind = TypeKind.Custom }; + return new(value.AssemblyQualifiedName!, TypeKind.Custom); } /// - /// Converts an unmanaged to a managed . + /// Converts a to a managed . /// - /// The unmanaged value. + /// The value. /// The managed value [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Any types which are trimmed are not used by managed user code and there is fallback logic to handle that.")] - public static global::System.Type? ConvertToManaged(Type value) + public static global::System.Type? FromTransientTypeReference(TransientTypeReference value) { - ReadOnlySpan typeName = HStringMarshaller.ConvertToManagedUnsafe(value.Name); + ReadOnlySpan typeName = value.Name; // Just return 'null' if we somehow received a default value if (typeName.IsEmpty) @@ -333,7 +500,7 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR publicType.IsAssignableTo(typeof(global::System.Exception)) || publicType.IsAssignableTo(typeof(global::System.Type))) { - return NoMetadataTypeInfo.GetOrCreate(typeName); + return new NoMetadataTypeInfo(typeName.ToString()); } if (publicType.IsValueType) @@ -375,35 +542,12 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR // returned implementation will just throw an exception for all unsupported operations on it. if (type is null && value.Kind is TypeKind.Metadata) { - return NoMetadataTypeInfo.GetOrCreate(typeName); + return new NoMetadataTypeInfo(typeName.ToString()); } // Return whatever result we managed to get from the cache return type; } - - /// - public static WindowsRuntimeObjectReferenceValue BoxToUnmanaged(global::System.Type? value) - { - return value is null ? default : new((void*)WindowsRuntimeComWrappers.Default.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.None, in WellKnownWindowsInterfaceIIDs.IID_IReferenceOfType)); - } - - /// - public static global::System.Type? UnboxToManaged(void* value) - { - Type? abi = WindowsRuntimeValueTypeMarshaller.UnboxToManaged(value); - - return abi.HasValue ? ConvertToManaged(abi.GetValueOrDefault()) : null; - } - - /// - /// Disposes resources associated with an unmanaged value. - /// - /// The unmanaged value to dispose. - public static void Dispose(Type value) - { - HStringMarshaller.Free(value.Name); - } } /// @@ -591,18 +735,6 @@ private static HRESULT get_Value(void* thisPtr, Type* result) /// file sealed class NoMetadataTypeInfo : TypeInfo { - /// - /// The cache of instances. - /// - /// - /// This cache is mostly only used by XAML, meaning it should pretty much always be accessed from the UI thread. - /// Because of this, we can set the concurrency level to just '1', to reduce the memory use from this dictionary. - /// - private static readonly ConcurrentDictionary NoMetadataTypeCache = new( - concurrencyLevel: 1, - capacity: 32, - comparer: StringComparer.Ordinal); - /// /// The full name of the type missing metadata information. /// @@ -612,32 +744,11 @@ private static HRESULT get_Value(void* thisPtr, Type* result) /// Creates a new instance with the specified parameters. /// /// The full name of the type missing metadata information. - private NoMetadataTypeInfo(string fullName) + public NoMetadataTypeInfo(string fullName) { _fullName = fullName; } - /// - /// Gets a cached instance for the specified type name. - /// - /// The full name of the type missing metadata information. - /// The resulting instance. - public static NoMetadataTypeInfo GetOrCreate(ReadOnlySpan fullName) - { - // Try to lookup an existing instance first, to skip allocating a 'string' if we can - if (NoMetadataTypeCache.GetAlternateLookup>().TryGetValue(fullName, out NoMetadataTypeInfo? existing)) - { - return existing; - } - - NoMetadataTypeInfo typeInfo = new(fullName.ToString()); - - // The type instance was not in the cache, so try to add it now. We perform this lookup - // with the 'string' instance we created to initialize the new 'NoMetadataTypeInfo' value - // we'trying to add to the cache, so that if we win the race, we only allocate it once. - return NoMetadataTypeCache.GetOrAdd(typeInfo._fullName, typeInfo); - } - /// public override Assembly Assembly => throw new NotSupportedException(); From a516f717d0c39bb7f0003cab942a2f2a2497ba46 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 15:29:17 -0800 Subject: [PATCH 03/10] Add EnableXamlTypeMarshalling feature switch Introduce a new feature switch in WindowsRuntimeFeatureSwitches to control marshalling of Type instances for XAML projections. Adds the configuration constant CSWINRT_ENABLE_XAML_TYPE_MARSHALLING and the EnableXamlTypeMarshalling property (FeatureSwitchDefinition) with a default of true, along with XML documentation. --- .../Properties/WindowsRuntimeFeatureSwitches.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs index d4279eeaa..c636be5b4 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs @@ -33,6 +33,11 @@ internal static class WindowsRuntimeFeatureSwitches /// private const string UseWindowsUIXamlProjectionsPropertyName = "CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS"; + /// + /// The configuration property name for . + /// + private const string EnableXamlTypeMarshallingPropertyName = "CSWINRT_ENABLE_XAML_TYPE_MARSHALLING"; + /// /// Gets a value indicating whether or not manifest free WinRT activation is supported (defaults to ). /// @@ -51,6 +56,12 @@ internal static class WindowsRuntimeFeatureSwitches [FeatureSwitchDefinition(UseWindowsUIXamlProjectionsPropertyName)] public static bool UseWindowsUIXamlProjections { get; } = GetConfigurationValue(UseWindowsUIXamlProjectionsPropertyName, defaultValue: false); + /// + /// Gets a value indicating whether marshalling instances is supported. + /// + [FeatureSwitchDefinition(EnableXamlTypeMarshallingPropertyName)] + public static bool EnableXamlTypeMarshalling { get; } = GetConfigurationValue(EnableXamlTypeMarshallingPropertyName, defaultValue: true); + /// /// Gets a configuration value for a specified property. /// From 9ec52d83b88a03c5a4c679a9e67cf0168f9af919 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 15:29:36 -0800 Subject: [PATCH 04/10] Guard Type marshalling behind feature switch Make System.Type marshalling conditional on WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling. Added runtime checks in TypeMarshaller ConvertFrom/ConvertTo to throw a NotSupportedException when XAML type marshalling is disabled, and added TypeExceptions.ThrowNotSupportedExceptionForMarshallingDisabled to provide a clear error message. Updated WindowsRuntimeComWrappers to only return Type marshalling info when the feature switch is enabled to avoid rooting extra metadata/type-mapping code and reduce binary size. --- src/WinRT.Runtime2/ABI/System/Type.cs | 23 +++++++++++++++++++ .../WindowsRuntimeMarshallingInfo.cs | 7 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Runtime2/ABI/System/Type.cs b/src/WinRT.Runtime2/ABI/System/Type.cs index b211a18c5..6733b7d22 100644 --- a/src/WinRT.Runtime2/ABI/System/Type.cs +++ b/src/WinRT.Runtime2/ABI/System/Type.cs @@ -101,6 +101,11 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR { ArgumentNullException.ThrowIfNull(value); + if (!WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling) + { + TypeExceptions.ThrowNotSupportedExceptionForMarshallingDisabled(); + } + ManagedTypeReference typeReference = TypeNameCache.TypeToTypeNameMap.GetOrAdd(value, UncachedTypeMarshaller.ToManagedTypeReference); reference = new TypeReference { Name = typeReference.Name, Kind = typeReference.Kind }; @@ -113,6 +118,11 @@ public static void ConvertToUnmanagedUnsafe(global::System.Type value, out TypeR /// The managed value public static global::System.Type? ConvertToManaged(Type value) { + if (!WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling) + { + TypeExceptions.ThrowNotSupportedExceptionForMarshallingDisabled(); + } + ReadOnlySpan typeName = HStringMarshaller.ConvertToManagedUnsafe(value.Name); // Get the alternate lookup from the cache first and use it to try to retrieve a cached 'Type' @@ -569,6 +579,19 @@ public static void ThrowArgumentExceptionForNullType(Type type) $"to be removed. To work around the issue, consider using the '[DynamicDependency]' attribute over the method causing this exception to eventually be thrown. " + $"You can see the API docs for this attribute here: https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute."); } + + /// + /// Throws a if marshalling support is disabled. + /// + [DoesNotReturn] + [StackTraceHidden] + public static void ThrowNotSupportedExceptionForMarshallingDisabled() + { + throw new NotSupportedException( + $"Support for marshalling 'System.Type' values is disabled (make sure that the 'CsWinRTEnableXamlTypeMarshalling' property is not set to 'false'). " + + $"In this configuration, marshalling a 'System.Type' value directly to native code or to managed will always fail. Additionally, marshalling a " + + $"boxed 'System.Type' object as an untyped parameter for a Windows Runtime API will result in the CCW using the same layot as for 'object'."); + } } /// diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs index 7d9cc2f79..6b7c92d2f 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs @@ -471,7 +471,12 @@ public static WindowsRuntimeMarshallingInfo GetOpaqueInfo(object instance) // That is, when e.g. doing 'typeof(Foo)', the actual object is some 'RuntimeType' object itself (non public). if (instance is Type) { - return GetInfo(typeof(Type)); + // Only enable this marshalling if the feature switch is enabled, to minimize size. Supporting 'Type' + // marshalling actually roots a significant amount of additional code, such as the metadata type map. + if (WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling) + { + return WindowsRuntimeMarshallingInfo.GetInfo(typeof(Type)); + } } // For all other cases, we fallback to the marshalling info for 'object'. This is the From d60608be822ba7c5dc8a2787f49680a3fe511f7b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 15:52:17 -0800 Subject: [PATCH 05/10] Remove default CsWinRT feature switches Delete default property values and their RuntimeHostConfigurationOption entries for several CsWinRT feature switches in nuget/Microsoft.Windows.CsWinRT.targets. Removed properties: CsWinRTEnableDynamicObjectsSupport, CsWinRTUseExceptionResourceKeys, CsWinRTEnableDefaultCustomTypeMappings, CsWinRTEnableICustomPropertyProviderSupport, and CsWinRTEnableIReferenceSupport. This cleans up obsolete/unused runtime feature switches while leaving other configuration options (e.g. IDynamicInterfaceCastable, ManifestFreeActivation) intact. --- nuget/Microsoft.Windows.CsWinRT.targets | 30 ------------------------- 1 file changed, 30 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 661c3d027..02246e483 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -568,11 +568,6 @@ $(CsWinRTInternalProjection) - true - false - true - true - true true true false @@ -590,31 +585,6 @@ $(CsWinRTInternalProjection) --> - - - - - - - - - - - - - - - Date: Tue, 3 Feb 2026 15:53:21 -0800 Subject: [PATCH 06/10] Add XAML type marshalling runtime option Introduce an MSBuild property CsWinRTEnableXamlTypeMarshalling (default true) and map it to the runtime host configuration option CSWINRT_ENABLE_XAML_TYPE_MARSHALLING in nuget/Microsoft.Windows.CsWinRT.targets. This exposes a build-time switch to enable/disable XAML type marshalling behavior alongside the other CsWinRT runtime switches. --- nuget/Microsoft.Windows.CsWinRT.targets | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 02246e483..887eabe10 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -571,6 +571,7 @@ $(CsWinRTInternalProjection) true true false + true false + Date: Tue, 3 Feb 2026 16:06:19 -0800 Subject: [PATCH 07/10] Add feature switch for IDynamicInterfaceCastable Introduce a configurable feature switch to control IDynamicInterfaceCastable support so the runtime can be trimmed when this capability is not needed. - nuget: rename the RuntimeHostConfigurationOption to CSWINRT_ENABLE_IDYNAMICINTERFACECASTABLE_SUPPORT and wire it to the CsWinRTEnableIDynamicInterfaceCastableSupport property. - src/WinRT.Runtime2: add EnableIDynamicInterfaceCastableSupport feature switch and default it to true. - src/WinRT.Runtime2/WindowsRuntimeObject: gate IDynamicInterfaceCastable APIs with the feature switch (throw NotSupportedException or return false when disabled) to allow trimming and to ensure unreachable paths can be removed. Also include a small doc comment polish for an IEnumerable reference. --- nuget/Microsoft.Windows.CsWinRT.targets | 9 +++---- .../WindowsRuntimeFeatureSwitches.cs | 11 ++++++++ src/WinRT.Runtime2/WindowsRuntimeObject.cs | 26 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 887eabe10..b36783bc9 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -580,14 +580,11 @@ $(CsWinRTInternalProjection) --> - + - - + diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs index c636be5b4..c3bf6048b 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs @@ -38,6 +38,11 @@ internal static class WindowsRuntimeFeatureSwitches /// private const string EnableXamlTypeMarshallingPropertyName = "CSWINRT_ENABLE_XAML_TYPE_MARSHALLING"; + /// + /// The configuration property name for . + /// + private const string EnableIDynamicInterfaceCastableSupportPropertyName = "CSWINRT_ENABLE_IDYNAMICINTERFACECASTABLE_SUPPORT"; + /// /// Gets a value indicating whether or not manifest free WinRT activation is supported (defaults to ). /// @@ -62,6 +67,12 @@ internal static class WindowsRuntimeFeatureSwitches [FeatureSwitchDefinition(EnableXamlTypeMarshallingPropertyName)] public static bool EnableXamlTypeMarshalling { get; } = GetConfigurationValue(EnableXamlTypeMarshallingPropertyName, defaultValue: true); + /// + /// Gets a value indicating whether or not should be supported by RCW types (defaults to ). + /// + [FeatureSwitchDefinition(EnableIDynamicInterfaceCastableSupportPropertyName)] + public static bool EnableIDynamicInterfaceCastableSupport { get; } = GetConfigurationValue(EnableIDynamicInterfaceCastableSupportPropertyName, defaultValue: true); + /// /// Gets a configuration value for a specified property. /// diff --git a/src/WinRT.Runtime2/WindowsRuntimeObject.cs b/src/WinRT.Runtime2/WindowsRuntimeObject.cs index d5e115baa..a00e8f19c 100644 --- a/src/WinRT.Runtime2/WindowsRuntimeObject.cs +++ b/src/WinRT.Runtime2/WindowsRuntimeObject.cs @@ -515,6 +515,20 @@ internal bool TryGetObjectReferenceForIEnumerableInterfaceInstance([NotNullWhen( /// RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTypeHandle interfaceType) { + if (!WindowsRuntimeFeatureSwitches.EnableIDynamicInterfaceCastableSupport) + { + [DoesNotReturn] + [StackTraceHidden] + static void ThrowNotSupportedException() + { + throw new NotSupportedException( + $"Support for 'IDynamicInterfaceCastable' is disabled (make sure that the 'CsWinRTEnableIDynamicInterfaceCastableSupport' property is not set to 'false'). " + + $"In this configuration, runtime casts on Windows Runtime objects will only work if the managed object implements the target interface in metadata."); + } + + ThrowNotSupportedException(); + } + Type type = Type.GetTypeFromHandle(interfaceType)!; // If we can resolve the implementation type through the Windows Runtime infrastructure, return it @@ -538,6 +552,11 @@ RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTy /// bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented) { + if (!WindowsRuntimeFeatureSwitches.EnableIDynamicInterfaceCastableSupport) + { + return false; + } + return TryGetCastResult( interfaceType: interfaceType, implementationType: out _, @@ -676,6 +695,13 @@ private bool LookupDynamicInterfaceCastableImplementationInfo(RuntimeTypeHandle { castResult = null; + // Also fail from here if the feature switch is disabled, to ensure that 'DynamicInterfaceCastableImplementationInfo' + // can fully be trimmed. In theory this path shouldn't be reachable if the feature is disabled, but this can help. + if (!WindowsRuntimeFeatureSwitches.EnableIDynamicInterfaceCastableSupport) + { + return false; + } + Type type = Type.GetTypeFromHandle(interfaceType)!; // If we can't resolve the implementation info at all, the cast can't possibly succeed From 89e9cebaf5587d740433865093fa5316a926852e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 16:10:50 -0800 Subject: [PATCH 08/10] Extract IDynamicInterfaceCastable exception stub Replace the inline ThrowNotSupportedException local with a file-scoped WindowsRuntimeObjectExceptions.ThrowNotSupportedException and update IDynamicInterfaceCastable methods to perform an early feature-switch check. This centralizes the NotSupportedException stub (DoesNotReturn/StackTraceHidden) to improve trimming and reuse, and ensures IsInterfaceImplemented throws the same descriptive NotSupportedException when throwIfNotImplemented is true. --- src/WinRT.Runtime2/WindowsRuntimeObject.cs | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/WinRT.Runtime2/WindowsRuntimeObject.cs b/src/WinRT.Runtime2/WindowsRuntimeObject.cs index a00e8f19c..ae9bf171e 100644 --- a/src/WinRT.Runtime2/WindowsRuntimeObject.cs +++ b/src/WinRT.Runtime2/WindowsRuntimeObject.cs @@ -515,18 +515,10 @@ internal bool TryGetObjectReferenceForIEnumerableInterfaceInstance([NotNullWhen( /// RuntimeTypeHandle IDynamicInterfaceCastable.GetInterfaceImplementation(RuntimeTypeHandle interfaceType) { + // Fail immediately if the feature switch is disabled, to ensure all related code can be trimmed if (!WindowsRuntimeFeatureSwitches.EnableIDynamicInterfaceCastableSupport) { - [DoesNotReturn] - [StackTraceHidden] - static void ThrowNotSupportedException() - { - throw new NotSupportedException( - $"Support for 'IDynamicInterfaceCastable' is disabled (make sure that the 'CsWinRTEnableIDynamicInterfaceCastableSupport' property is not set to 'false'). " + - $"In this configuration, runtime casts on Windows Runtime objects will only work if the managed object implements the target interface in metadata."); - } - - ThrowNotSupportedException(); + WindowsRuntimeObjectExceptions.ThrowNotSupportedException(); } Type type = Type.GetTypeFromHandle(interfaceType)!; @@ -552,8 +544,17 @@ static void ThrowNotSupportedException() /// bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented) { + // Early feature switch check to improve trimming (same as above) if (!WindowsRuntimeFeatureSwitches.EnableIDynamicInterfaceCastableSupport) { + // If we should throw, explicitly throw the same exception as from 'GetInterfaceImplementation', rather than + // just returning 'false' and letting the runtime throw an 'InvalidCastException'. This allows develoers to + // more easily understand why a given runtime cast might be failing under different configurations. + if (throwIfNotImplemented) + { + WindowsRuntimeObjectExceptions.ThrowNotSupportedException(); + } + return false; } @@ -900,4 +901,22 @@ private sealed class DynamicInterfaceCastableResult /// A dummy type to use for caching adaptive object references in . /// private static class IEnumerableInstance; +} + +/// +/// Exception stubs for . +/// +file static class WindowsRuntimeObjectExceptions +{ + /// + /// Throws a if support for is disabled. + /// + [DoesNotReturn] + [StackTraceHidden] + public static void ThrowNotSupportedException() + { + throw new NotSupportedException( + $"Support for 'IDynamicInterfaceCastable' is disabled (make sure that the 'CsWinRTEnableIDynamicInterfaceCastableSupport' property is not set to 'false'). " + + $"In this configuration, runtime casts on Windows Runtime objects will only work if the managed object implements the target interface in metadata."); + } } \ No newline at end of file From 1f93825c118e08e9ab81bcd1f2dc4e0402b282ae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 3 Feb 2026 16:38:35 -0800 Subject: [PATCH 09/10] Fix typos Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WinRT.Runtime2/ABI/System/Type.cs | 2 +- src/WinRT.Runtime2/WindowsRuntimeObject.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Runtime2/ABI/System/Type.cs b/src/WinRT.Runtime2/ABI/System/Type.cs index 6733b7d22..50e5b0290 100644 --- a/src/WinRT.Runtime2/ABI/System/Type.cs +++ b/src/WinRT.Runtime2/ABI/System/Type.cs @@ -590,7 +590,7 @@ public static void ThrowNotSupportedExceptionForMarshallingDisabled() throw new NotSupportedException( $"Support for marshalling 'System.Type' values is disabled (make sure that the 'CsWinRTEnableXamlTypeMarshalling' property is not set to 'false'). " + $"In this configuration, marshalling a 'System.Type' value directly to native code or to managed will always fail. Additionally, marshalling a " + - $"boxed 'System.Type' object as an untyped parameter for a Windows Runtime API will result in the CCW using the same layot as for 'object'."); + $"boxed 'System.Type' object as an untyped parameter for a Windows Runtime API will result in the CCW using the same layout as for 'object'."); } } diff --git a/src/WinRT.Runtime2/WindowsRuntimeObject.cs b/src/WinRT.Runtime2/WindowsRuntimeObject.cs index ae9bf171e..405853c79 100644 --- a/src/WinRT.Runtime2/WindowsRuntimeObject.cs +++ b/src/WinRT.Runtime2/WindowsRuntimeObject.cs @@ -548,7 +548,7 @@ bool IDynamicInterfaceCastable.IsInterfaceImplemented(RuntimeTypeHandle interfac if (!WindowsRuntimeFeatureSwitches.EnableIDynamicInterfaceCastableSupport) { // If we should throw, explicitly throw the same exception as from 'GetInterfaceImplementation', rather than - // just returning 'false' and letting the runtime throw an 'InvalidCastException'. This allows develoers to + // just returning 'false' and letting the runtime throw an 'InvalidCastException'. This allows developers to // more easily understand why a given runtime cast might be failing under different configurations. if (throwIfNotImplemented) { From ca8a4f08a212f2051632d8813e7def754b14efac Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Feb 2026 21:01:54 -0800 Subject: [PATCH 10/10] Guard Type marshalling with feature switch Reorder the Type marshalling check to first verify WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling so the 'Type' cast can be trimmed and avoid rooting the metadata type map (reducing binary size). Also simplify the GetInfo call to use the local method (GetInfo(typeof(Type))). Comments updated to reflect the new ordering. --- .../WindowsRuntimeMarshallingInfo.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs index 6b7c92d2f..06c31b568 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs @@ -466,16 +466,18 @@ public static WindowsRuntimeMarshallingInfo GetOpaqueInfo(object instance) return GetInfo(typeof(Exception)); } - // Special case for 'Type' instances too. This is needed even without considering custom user-defined types - // (which shouldn't really be common anyway), because 'Type' itself is just a base type and not instantiated. - // That is, when e.g. doing 'typeof(Foo)', the actual object is some 'RuntimeType' object itself (non public). - if (instance is Type) + // Only enable this marshalling if the feature switch is enabled, to minimize size. Supporting 'Type' + // marshalling actually roots a significant amount of additional code, such as the metadata type map. + // We check the feature switch first to allow the 'Type' cast itself to be trimmed as well. This is + // something that can actually impact binary size, since it will root the type map entries for 'Type'. + if (WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling) { - // Only enable this marshalling if the feature switch is enabled, to minimize size. Supporting 'Type' - // marshalling actually roots a significant amount of additional code, such as the metadata type map. - if (WindowsRuntimeFeatureSwitches.EnableXamlTypeMarshalling) + // Special case for 'Type' instances too. This is needed even without considering custom user-defined types + // (which shouldn't really be common anyway), because 'Type' itself is just a base type and not instantiated. + // That is, when e.g. doing 'typeof(Foo)', the actual object is some 'RuntimeType' object itself (non public). + if (instance is Type) { - return WindowsRuntimeMarshallingInfo.GetInfo(typeof(Type)); + return GetInfo(typeof(Type)); } }