dotnet / dotnet/runtime

[API Proposal]: well-defined user state for `ComWrappers.CreateObject` to pass target type from `WeakReference<T>`

Open
#120,551 5 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.Runtime.InteropServices partner-impact
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Background and motivation

The current implementation of `WeakReference` has an issue that various Windows components started hitting (cc. @jevansaks) and we had to work around, where trying to retrieve the target will sometimes throw an `InvalidCastException` in CsWinRT scenarios. The issue stems from this code:

https://github.com/dotnet/runtime/blob/ee4c4d9532d698a77d3cae9211562459b48b2454/src/libraries/System.Private.CoreLib/src/System/ComAwareWeakReference.cs#L107-L128

This is where things can break down in WinRT scenarios:
- You call a WinRT API that returns a some unsealed type (say, `Button`)
- At runtime, the object we get is some **non projected** derived type (say, `PrivateButton : Button`)
- Because the type is not projected, `GetRuntimeClassName` will be some interface name (e.g. `IButton`)
- We create a `WeakReference` on this thing
- At some point, the RCW is collected, because nobody is keeping it alive
- You try to resolve the target from the weak reference
- The RCW is gone, so the weak reference tries to rehydrate the RCW by calling `CreateObject`
- We have no static type info here anymore, so we have to rely on the runtime class name
- The runtime class name doesn't map to any concrete type, so we just create some `IInspectable`
- The weak reference then does the `(T)` cast (e.g. to `Button` here), that throws `InvalidCastException` 💥

This is not great both because it throws, but also because the crash is from inside `WeakReference`, so it's very confusing. `WeakReference` should be able to pass the generic context info back to `CreateObject`, so it can create the right RCW.

### API Proposal

My idea is to build on top of the work we did in #113622, via a new public user state type.

Something like this:

```csharp
namespace System.Runtime.InteropServices;

public sealed class CreateObjectContext
{
public Type TargetType { get; }
}
```

### API Usage

The way this would work is that `WeakReference` would track **the actual type of the RCW upon construction**, and it would later pass it to `CreateObject` via its new overload taking a user state. Then the `ComWrappers` implementation would be able to see this type info and use it to construct an RCW of the right type, so that things would keep working correctly even in case of rehydration.

> [!IMPORTANT]
> We can't just pass `typeof(T)` as the target type, as it's completely possible to have differently typed `WeakReference` instances for the same RCW. The target type passed to `ComWrappers` has to be the real runtime type of the original RCW.

### Alternative Designs

We tried working around this in CsWinRT (https://github.com/microsoft/CsWinRT/pull/2043), but the fix is also technically broken (since this will still crash if you get different RCWs that happen to have the same runtime class name, which is completely possible for interface types). As far as we're aware there is simply no way to correctly fix this on our end, and it needs something like the above proposal on the weak reference side itself.

### Risks

Low risk, but it might make the layout of `WeakReference` larger due to the need to store the target type somewhere.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.