diff --git a/build/AzurePipelineTemplates/CsWinRT-FunctionalTest-Steps.yml b/build/AzurePipelineTemplates/CsWinRT-FunctionalTest-Steps.yml index 08bdf12ba..043d95a49 100644 --- a/build/AzurePipelineTemplates/CsWinRT-FunctionalTest-Steps.yml +++ b/build/AzurePipelineTemplates/CsWinRT-FunctionalTest-Steps.yml @@ -17,6 +17,7 @@ parameters: - DerivedClassAsBaseClass - CCW - NativeExposedType + - OpaqueRcwMarshalling - TypeMarshaling - ImplementWinMDTypes - UserDefinedTypeNames diff --git a/docs/aot-trimming.md b/docs/aot-trimming.md index 3174c2592..8d541d2d2 100644 --- a/docs/aot-trimming.md +++ b/docs/aot-trimming.md @@ -31,6 +31,7 @@ Feature switches are set as MSBuild properties in your project file: | `CsWinRTEnableMarshalingTypeValidation` | `false` | Validates the `MarshalingType` to determine whether the metadata is the same as the implementation. | | `CsWinRTEnableDefaultCustomPropertyProviderSupport` | `true` | Adds a default, type-only `ICustomPropertyProvider` to generated managed CCWs and the runtime's shared opaque-object CCW, without replacing explicit providers. | | `CsWinRTSuppressCustomPropertyNotSupportedException` | `false` | Returns null instead of reporting `NotSupportedException` when the default provider cannot supply a property or indexer. | +| `CsWinRTEnableOpaqueRcwMarshalling` | `false` | Allows projected runtime class instances (RCWs) that are not native-exposed to be marshalled to native code as opaque CCWs, instead of reporting `NotSupportedException`. | ## Selecting dynamic casting for exclusive interfaces @@ -40,6 +41,31 @@ Reference projection packages preserve the exact effective selection as generato Opting out avoids the selected interface's IDIC shim and type-map association, but does not remove ABI helpers or CCW implementations still needed by public/default/overridable interfaces. In the single-method exclusive-interface regression fixture, opting in adds exactly one shim type, one DIM method, and one assembly type-map association. Final binary size depends on trimming and reachability. +## Marshalling projected runtime classes as opaque CCWs + +Projected runtime classes are backed by native objects, so passing one to native code unwraps the native object and no CCW is involved. Native code can still ask the runtime for a CCW around the managed wrapper (RCW) through `ComWrappers`. For example, on some Windows versions XAML asks the reference tracker host to wrap a projected object it was handed, such as a `Frame` or a `Grid` set as `Window.Content`. A projected class has no CCW vtables unless it is [native-exposed](interop.md#create-ccw), so by default this request is reported as a `NotSupportedException`, and the native caller can fail fast. + +To expose these objects to native code as opaque CCWs instead, opt in by setting the following property in the project file: + +```xml + + true + +``` + +This defaults to `false` and sets the `CSWINRT_ENABLE_OPAQUE_RCW_MARSHALLING` runtime feature switch. The CCW is then the runtime's shared opaque-object CCW, the same one used for plain managed objects, so the generated marshalling code does not change and no type needs to be annotated. The behavior is as follows: + +- The CCW implements `IInspectable`, `IStringable`, `IWeakReferenceSource`, `IMarshal` and `IAgileObject`, plus `ICustomPropertyProvider` when `CsWinRTEnableDefaultCustomPropertyProviderSupport` is enabled. `GetIids` reports only these interfaces and `IUnknown`. +- Queries for any other interface, including the interfaces of the projected type, are forwarded by the RCW to the native object, so native code can still reach the interfaces that the native object implements. They fail if the native object does not implement them. +- `GetRuntimeClassName` reports the full name of the projected type, such as `Windows.Data.Json.JsonObject`. A plain opaque object reports `Object`. +- Native-exposed projected types are not affected. They keep the CCW vtables from their own `[WindowsRuntimeNativeExposedType]` annotation. +- Only `ComWrappers` requests are affected. Ordinary marshalling, such as passing the RCW to a projected method, still unwraps the native object and keeps the same object identity. +- Only projected runtime classes are covered, not interfaces, delegates or other types. + +The opaque CCW does not implement the interfaces of the projected type, and only forwards queries for them to the native object. Scenarios that need the CCW itself to implement additional interfaces still require `[assembly: WindowsRuntimeNativeExposedType(typeof(...))]`. This is the case for the covariant collection interfaces that XAML queries when a `DependencyObjectCollection` is used as an items source, as described in [Create CCW](interop.md#create-ccw). + +The switch is pay-for-play: when it is disabled, the trimmer and Native AOT remove the code behind it. + ## ICustomPropertyProvider support for XAML binding Non source-generated WinUI and UWP XAML binding scenarios (i.e., not `x:Bind`) such as `DisplayMemberPath` make use of `ICustomPropertyProvider`. CsWinRT 3.0 provides an AOT-safe source-generated implementation via the `[GeneratedCustomPropertyProvider]` attribute. diff --git a/docs/interop.md b/docs/interop.md index 3a1ae058c..ea0ed2c81 100644 --- a/docs/interop.md +++ b/docs/interop.md @@ -133,6 +133,8 @@ Native code can also need a CCW **around a projected native object's managed wra This supplies the wrapper's CCW vtables without replacing its RCW marshaller. Ordinary marshalling still unwraps the original native object, and native-to-managed conversion still creates the projected type. Collection adapters can wrap native enumerators when a required bindable or covariant iterator interface is unavailable; ordinary interface marshalling remains strict. +Projected classes that are not annotated this way have no CCW vtables, so a CCW request for one of them fails with a `NotSupportedException` by default. If native code only needs such objects as opaque references, set `CsWinRTEnableOpaqueRcwMarshalling` to `true` to expose them through the shared opaque-object CCW instead, as described in [Marshalling projected runtime classes as opaque CCWs](aot-trimming.md#marshalling-projected-runtime-classes-as-opaque-ccws). Annotated classes keep their own vtables, and scenarios that need additional interfaces, such as the covariant collection interfaces above, still need the annotation. + ### Release native pointer To release a native COM pointer. Unlike `Marshal.Release`, this method is null-safe: diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index ebdef6507..631222e78 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -655,6 +655,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. true true false + false + diff --git a/src/Tests/FunctionalTests/NativeExposedType/Program.cs b/src/Tests/FunctionalTests/NativeExposedType/Program.cs index bbb9a4f44..0b0143a47 100644 --- a/src/Tests/FunctionalTests/NativeExposedType/Program.cs +++ b/src/Tests/FunctionalTests/NativeExposedType/Program.cs @@ -47,6 +47,13 @@ private static int Main(string[] args) return 102; } + // Without a proxy, a CCW can't be created for a 'JsonObject' either, unless the opt-in opaque RCW marshalling + // feature is enabled. This project doesn't enable it, so the request must fail. + if (!IsOpaqueRcwMarshallingDisabled()) + { + return 103; + } + CollectionTests.Run(rcwFirst: args is ["--rcw-first"]); if (args is ["--xaml"]) @@ -57,6 +64,21 @@ private static int Main(string[] args) return 100; } + // Checks that a CCW is not created for an RCW of a projected type that is not native-exposed by default + private static unsafe bool IsOpaqueRcwMarshallingDisabled() + { + try + { + WindowsRuntimeMarshal.Free(WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(new JsonObject(), CreateComInterfaceFlags.TrackerSupport)); + + return false; + } + catch (NotSupportedException) + { + return true; + } + } + // Retrieves the proxy type map used by CsWinRT to resolve CCW marshalling info for managed objects [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The proxy type map is always preserved by the interop generator.")] private static IReadOnlyDictionary GetComWrappersProxyTypeMapping() diff --git a/src/Tests/FunctionalTests/OpaqueRcwMarshalling/OpaqueRcwMarshalling.csproj b/src/Tests/FunctionalTests/OpaqueRcwMarshalling/OpaqueRcwMarshalling.csproj new file mode 100644 index 000000000..abd09f421 --- /dev/null +++ b/src/Tests/FunctionalTests/OpaqueRcwMarshalling/OpaqueRcwMarshalling.csproj @@ -0,0 +1,18 @@ + + + + Exe + $(FunctionalTestsBuildTFMs) + x86;x64 + win-x86;win-x64 + $(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml + true + true + + + + + + + + diff --git a/src/Tests/FunctionalTests/OpaqueRcwMarshalling/Program.cs b/src/Tests/FunctionalTests/OpaqueRcwMarshalling/Program.cs new file mode 100644 index 000000000..37679a819 --- /dev/null +++ b/src/Tests/FunctionalTests/OpaqueRcwMarshalling/Program.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Windows.Data.Json; +using WindowsRuntime.InteropServices; +using WindowsRuntime.InteropServices.Marshalling; + +// Projected types that are native-exposed must keep using their own CCW proxy when opaque RCW marshalling +// is enabled, so this test also includes one, to verify that the opaque fallback doesn't take precedence. +[assembly: WindowsRuntimeNativeExposedType(typeof(JsonArray))] + +namespace OpaqueRcwMarshalling; + +internal static unsafe class Program +{ + private const int E_NOINTERFACE = unchecked((int)0x80004002); + + private static readonly Guid IJsonValueIid = new("A3219ECB-F0B3-4DCD-BEEE-19D48CD3ED1E"); + private static readonly Guid IJsonObjectIid = new("064E24DD-29C2-4F83-9AC1-9EE11578BEB3"); + private static readonly Guid IIterableObjectIid = new("092B849B-60B1-52BE-A44A-6FE8E933CBE4"); + + [STAThread] + private static int Main() + { + // Each of these RCWs is exposed to native code as an opaque CCW. The first and last cases use the same + // projected type, so that the second one also validates reusing the marshalling info that was cached. + CheckOpaqueRcw(new JsonObject(), JsonValueType.Object, "Windows.Data.Json.JsonObject", isJsonObject: true); + CheckOpaqueRcw(JsonValue.CreateNumberValue(42), JsonValueType.Number, "Windows.Data.Json.JsonValue", isJsonObject: false); + CheckOpaqueRcw(new JsonObject(), JsonValueType.Object, "Windows.Data.Json.JsonObject", isJsonObject: true); + + CheckNativeExposedRcw(new JsonArray()); + + return 100; + } + + private static void CheckOpaqueRcw(object rcw, JsonValueType expectedValueType, string expectedRuntimeClassName, bool isJsonObject) + { + // Native code (e.g. the XAML reference tracker host) can ask for a CCW for any RCW it was handed. This + // is what produces the CCW here too: it fails for projected types that are not native-exposed by default. + void* ccw = WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(rcw, CreateComInterfaceFlags.TrackerSupport); + + try + { + Check(WindowsRuntimeMarshal.IsReferenceToManagedObject(ccw), "The CCW must be a managed object wrapper."); + Check(WindowsRuntimeMarshal.TryGetManagedObject(ccw, out object managed) && ReferenceEquals(managed, rcw), "The CCW must wrap the original RCW."); + + CheckStandardInterfaces(ccw); + CheckForwardedInterfaces(ccw, rcw, expectedValueType, isJsonObject); + CheckIInspectable(ccw, expectedRuntimeClassName); + } + finally + { + WindowsRuntimeMarshal.Free(ccw); + GC.KeepAlive(rcw); + } + + // Regular marshalling must be unaffected, and keep unwrapping the native object instead of using the CCW + void* native = WindowsRuntimeMarshal.ConvertToUnmanaged(rcw); + + try + { + Check(!WindowsRuntimeMarshal.IsReferenceToManagedObject(native), "Regular marshalling must unwrap the native object."); + Check(ReferenceEquals(rcw, WindowsRuntimeMarshal.ConvertToManaged(native)), "A native round trip changed the RCW."); + } + finally + { + WindowsRuntimeMarshal.Free(native); + } + } + + private static void CheckStandardInterfaces(void* ccw) + { + // These are always implemented by the CCW itself, and must never be forwarded to the native object + foreach (Guid iid in (ReadOnlySpan)[WellKnownInterfaceIIDs.IID_IUnknown, WellKnownInterfaceIIDs.IID_IInspectable, WellKnownInterfaceIIDs.IID_IWeakReferenceSource]) + { + void* @interface = QueryInterface(ccw, in iid); + + try + { + Check(WindowsRuntimeMarshal.IsReferenceToManagedObject(@interface), $"The {iid} interface must resolve to the CCW."); + } + finally + { + WindowsRuntimeMarshal.Free(@interface); + } + } + + // These are also part of the CCW, but they can be forwarded to the native object when it implements them + foreach (Guid iid in (ReadOnlySpan)[WellKnownInterfaceIIDs.IID_IStringable, WellKnownInterfaceIIDs.IID_IAgileObject, WellKnownInterfaceIIDs.IID_IMarshal]) + { + WindowsRuntimeMarshal.Free(QueryInterface(ccw, in iid)); + } + + // An interface that is neither implemented by the CCW nor by the native object must not be available + Guid unknownIid = new("9C0D1F3C-6E19-4E2C-9A55-1B2C3D4E5F60"); + int hresult = Marshal.QueryInterface((nint)ccw, in unknownIid, out nint unknown); + + Check(hresult == E_NOINTERFACE && unknown == 0, "An unrelated interface must not be available."); + } + + private static void CheckForwardedInterfaces(void* ccw, object rcw, JsonValueType expectedValueType, bool isJsonObject) + { + // The CCW doesn't implement any interface from the projected type, but the RCW forwards the requests to + // the native object. This is how native code can still use the object it originally received, which + // can include XAML calling back into the object with interfaces it knows the native object implements. + void* jsonValue = QueryInterface(ccw, in IJsonValueIid); + + try + { + Check(!WindowsRuntimeMarshal.IsReferenceToManagedObject(jsonValue), "The IJsonValue interface must be forwarded to the native object."); + + JsonValueType valueType = default; + + Marshal.ThrowExceptionForHR(((delegate* unmanaged[MemberFunction])(*(void***)jsonValue)[6])(jsonValue, &valueType)); + Check(valueType == expectedValueType, $"The forwarded IJsonValue interface returned the wrong value type: {valueType}."); + Check(ReferenceEquals(rcw, WindowsRuntimeMarshal.ConvertToManaged(jsonValue)), "The forwarded native object resolved to a different RCW."); + } + finally + { + WindowsRuntimeMarshal.Free(jsonValue); + } + + int hresult = Marshal.QueryInterface((nint)ccw, in IJsonObjectIid, out nint jsonObject); + + if (isJsonObject) + { + Marshal.ThrowExceptionForHR(hresult); + Check(!WindowsRuntimeMarshal.IsReferenceToManagedObject((void*)jsonObject), "The IJsonObject interface must be forwarded to the native object."); + WindowsRuntimeMarshal.Free((void*)jsonObject); + } + else + { + Check(hresult == E_NOINTERFACE && jsonObject == 0, "The native object does not implement IJsonObject, so the request must fail."); + } + } + + private static void CheckIInspectable(void* ccw, string expectedRuntimeClassName) + { + ReadOnlySpan iids = GetIids(ccw); + + // The CCW only reports the standard interfaces for opaque objects (with 'IUnknown' last), which + // are the same ones used for any other opaque managed object. It can also have the property provider. + HashSet distinctIids = []; + + foreach (Guid iid in iids) + { + Check(iid != Guid.Empty && distinctIids.Add(iid), "GetIids returned an empty or duplicate interface."); + } + + Check(iids.Length is 6 or 7, $"GetIids returned an unexpected number of interfaces: {iids.Length}."); + Check(iids[^1] == WellKnownInterfaceIIDs.IID_IUnknown, "GetIids must have IUnknown as the last interface."); + Check( + distinctIids.IsSupersetOf([ + WellKnownInterfaceIIDs.IID_IUnknown, + WellKnownInterfaceIIDs.IID_IInspectable, + WellKnownInterfaceIIDs.IID_IStringable, + WellKnownInterfaceIIDs.IID_IWeakReferenceSource, + WellKnownInterfaceIIDs.IID_IMarshal, + WellKnownInterfaceIIDs.IID_IAgileObject]), + "GetIids must return the standard interfaces."); + + // Projected classes don't have a runtime class name attribute, so the projected type name is used + string runtimeClassName = GetRuntimeClassName(ccw); + + Check(runtimeClassName == expectedRuntimeClassName, $"GetRuntimeClassName returned an unexpected name: '{runtimeClassName}'."); + } + + private static void CheckNativeExposedRcw(JsonArray array) + { + void* ccw = WindowsRuntimeComWrappersMarshal.GetOrCreateComInterfaceForObject(array, CreateComInterfaceFlags.TrackerSupport); + + try + { + Check(WindowsRuntimeMarshal.TryGetManagedObject(ccw, out object managed) && ReferenceEquals(managed, array), "The CCW must wrap the native-exposed RCW."); + + // The proxy for the native-exposed type has priority, so the CCW must not be the opaque one + ReadOnlySpan iids = GetIids(ccw); + + Check(iids.Contains(IIterableObjectIid), "The native-exposed type must keep using the vtables from its own proxy."); + Check(GetRuntimeClassName(ccw) == "Windows.Data.Json.JsonArray", "The native-exposed type must report its projected type name."); + } + finally + { + WindowsRuntimeMarshal.Free(ccw); + GC.KeepAlive(array); + } + } + + private static Guid[] GetIids(void* instance) + { + void* inspectable = QueryInterface(instance, in WellKnownInterfaceIIDs.IID_IInspectable); + Guid* iids = null; + uint count = 0; + + try + { + Marshal.ThrowExceptionForHR(((delegate* unmanaged[MemberFunction])(*(void***)inspectable)[3])(inspectable, &count, &iids)); + + ReadOnlySpan interfaces = new(iids, checked((int)count)); + + return interfaces.ToArray(); + } + finally + { + Marshal.FreeCoTaskMem((nint)iids); + WindowsRuntimeMarshal.Free(inspectable); + } + } + + private static string GetRuntimeClassName(void* instance) + { + void* inspectable = QueryInterface(instance, in WellKnownInterfaceIIDs.IID_IInspectable); + void* runtimeClassName = null; + + try + { + Marshal.ThrowExceptionForHR(((delegate* unmanaged[MemberFunction])(*(void***)inspectable)[4])(inspectable, &runtimeClassName)); + + return HStringMarshaller.ConvertToManaged(runtimeClassName); + } + finally + { + HStringMarshaller.Free(runtimeClassName); + WindowsRuntimeMarshal.Free(inspectable); + } + } + + private static void* QueryInterface(void* instance, in Guid iid) + { + Marshal.ThrowExceptionForHR(Marshal.QueryInterface((nint)instance, in iid, out nint result)); + + return (void*)result; + } + + private static void Check(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } +} diff --git a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs index 8f5cf7923..2b4c515b2 100644 --- a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs +++ b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs @@ -56,6 +56,13 @@ namespace WindowsRuntime.InteropServices; /// Applying this attribute to a projected class type explicitly requests that a CCW vtable be generated for it, /// which would not normally be present, since the type is just a projection. /// +/// +/// Projected classes that only need to be exposed to native code as opaque objects do not need this attribute. +/// Setting the CsWinRTEnableOpaqueRcwMarshalling MSBuild property to true makes requests for a CCW +/// over any projected class without a CCW vtable use a shared opaque CCW, instead of being reported as not +/// supported. That CCW does not implement any of the interfaces of the projected type, so scenarios like the +/// one described above still require this attribute. +/// /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] public sealed class WindowsRuntimeNativeExposedTypeAttribute : Attribute diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs index 09862f13d..19b191282 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs @@ -297,6 +297,25 @@ bool InitializeIsMetadataType() } } + /// + /// Gets whether the current instance is for a projected Windows Runtime class. + /// + /// + /// Projected runtime classes are their own metadata provider. Proxies, value types, interfaces, and delegates + /// (which are classes too, but have their own CCW marshalling logic) are never considered projected classes. + /// + private bool IsProjectedClass + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return + ReferenceEquals(_metadataProviderType, _publicType) && + _metadataProviderType.IsClass && + _metadataProviderType.BaseType != typeof(MulticastDelegate); + } + } + /// /// Tries to get a instance for a given runtime class name. /// @@ -592,20 +611,24 @@ bool Load([NotNullWhen(true)] out WindowsRuntimeComWrappersMarshallerAttribute? /// /// The resulting instance. /// Thrown if no instance could be resolved. - /// This method is meant to be used when preparing CCW vtables for managed types. + /// + /// This method is meant to be used when preparing CCW vtables for managed types. + /// + /// If is enabled, projected runtime classes that + /// have no CCW vtables (i.e. that are not native-exposed) are marshalled as opaque objects instead of throwing. + /// + /// public WindowsRuntimeVtableInfo GetVtableInfo() { // Initialize the vtable if not already present (it is safe to do this concurrently) [MethodImpl(MethodImplOptions.NoInlining)] unsafe WindowsRuntimeVtableInfo InitializeVtableInfo() { + bool isProjectedClass = IsProjectedClass; + // Native-exposed projected types have a separate CCW proxy. Only use it for vtables: // replacing the projected type's marshaller would break RCW creation and native unwrapping. - // Already-resolved proxies, value types, interfaces, and delegates cannot need this lookup. - if (ReferenceEquals(_metadataProviderType, _publicType) && - _metadataProviderType.IsClass && - _metadataProviderType.BaseType != typeof(MulticastDelegate) && - ProxyTypeMapping.TryGetValue(_metadataProviderType, out Type? proxyType)) + if (isProjectedClass && ProxyTypeMapping.TryGetValue(_metadataProviderType, out Type? proxyType)) { WindowsRuntimeMarshallingInfo proxyInfo = TypeToMarshallingInfoTable.GetOrAdd(proxyType, CreateMarshallingInfoCallback)!; @@ -626,6 +649,15 @@ unsafe WindowsRuntimeVtableInfo InitializeVtableInfo() // failure, rather than having that method throw and then catch and re-throw from here. if (vtableEntries is null) { + // Projected runtime classes without a native-exposure proxy only have an RCW-only marshaller. If opaque + // RCW marshalling is enabled, expose them with the shared vtables for opaque objects instead of failing. + // The resulting CCW only implements the standard interfaces, and the projected class itself forwards any + // other 'QueryInterface' request to its native object. Any other kind of type keeps failing below. + if (WindowsRuntimeFeatureSwitches.EnableOpaqueRcwMarshalling && isProjectedClass) + { + return _vtableInfo ??= GetInfo(typeof(object)).GetVtableInfo(); + } + // Throws an exception with a message that includes the type of the object we failed to marshal [DoesNotReturn] [StackTraceHidden] @@ -652,11 +684,24 @@ void ThrowNotSupportedException(WindowsRuntimeComWrappersMarshallerAttribute com /// /// The resulting runtime class name. /// Thrown if no runtime class name could be resolved. - /// This method is only meant to be used on managed types passed to native. + /// + /// This method is only meant to be used on managed types passed to native. + /// + /// If is enabled, projected runtime classes + /// (which have no runtime class name info, as it is normally provided by their native object) report their type name. + /// + /// public string GetRuntimeClassName() { if (!TryGetRuntimeClassName(out string? runtimeClassName)) { + // Projected runtime classes that are exposed as opaque objects only have their projected type, which always + // matches the name of the Windows Runtime class. This is the same fallback used when marshalling 'Type' values. + if (WindowsRuntimeFeatureSwitches.EnableOpaqueRcwMarshalling && IsProjectedClass) + { + return PublicType.FullName!; + } + // Analogous validation as for when retrieving the marshaller attribute [DoesNotReturn] [StackTraceHidden] diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs index fed66ee2a..cf832fcbe 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeFeatureSwitches.cs @@ -65,6 +65,11 @@ internal static class WindowsRuntimeFeatureSwitches /// private const string SuppressCustomPropertyNotSupportedExceptionPropertyName = "CSWINRT_SUPPRESS_CUSTOM_PROPERTY_NOT_SUPPORTED_EXCEPTION"; + /// + /// The configuration property name for . + /// + private const string EnableOpaqueRcwMarshallingPropertyName = "CSWINRT_ENABLE_OPAQUE_RCW_MARSHALLING"; + /// /// Gets a value indicating whether or not manifest free WinRT activation is supported (defaults to ). /// @@ -119,6 +124,12 @@ internal static class WindowsRuntimeFeatureSwitches [FeatureSwitchDefinition(SuppressCustomPropertyNotSupportedExceptionPropertyName)] public static bool SuppressCustomPropertyNotSupportedException { get; } = GetConfigurationValue(SuppressCustomPropertyNotSupportedExceptionPropertyName, defaultValue: false); + /// + /// Gets a value indicating whether projected runtime class instances that cannot be marshalled to native code are exposed as opaque CCWs instead of reporting (defaults to ). + /// + [FeatureSwitchDefinition(EnableOpaqueRcwMarshallingPropertyName)] + public static bool EnableOpaqueRcwMarshalling { get; } = GetConfigurationValue(EnableOpaqueRcwMarshallingPropertyName, defaultValue: false); + /// /// Gets a configuration value for a specified property. /// diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 7a0281af6..fb706ef14 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -467,6 +467,12 @@ + + + + + +