Skip to content

[API Proposal]: 'ComWrappers.GetOrCreateObjectForComInstance' variant returning 'null' without throwing #113591

@Sergio0694

Description

@Sergio0694

Background and motivation

Currently, CsWinRT uses ComWrappers.GetOrCreateObjectForComInstance to produce RCWs for native objects. However, this is inefficient when it comes to values which are "boxed" (ie. IReference<T> and IReferenceArray<T> objects), as in that case what we want to do is to immediately "unbox" them, and return the inner value to the user. To make some examples:

  • IReference<Int32> -> we return a boxed int
  • IReference<SomeDelegate> -> we return a SomeDelegate instance

However, the only way for us to know which type of object we're dealing with, is to perform some additional work on it (eg. calling GetRuntimeClassName on it, after doing a QueryInterface for IInspectable). This is fine, since we'd need to do this anyway to figure out what managed type to create. The problem is that once we're inside ComWrappers.CreateObject, we have to return some valid instance, or ComWrappers will throw an exception. To avoid the problem, what we're currently doing is to return some dummy "wrapper" object as RCW, and then we immediately unwrap it on the other side.

This works, but it's far from ideal, for the following reasons:

  • It adds overhead due to creating this dummy object and having ComWrappers register it, and track it
    • We genuinely don't care about this object at all, so this is purely wasted work
  • It forces us to have a ConditionalWeakTable<,> to extend the lifetime of each native object to that of our returned dummy object. This is to avoid "pointer reuse" bugs (where native code could reuse the same pointer address for another object, if we didn't keep it alive). That is, we get:
    • Extra complexity to handle this caching
    • Unnecessary overhead for managing this table, plus creating some managed, finalizable object to release the object
    • We're keeping the native objects alive for literally no reason (we don't need them)
  • It also makes our code in CsWinRT more complicated and clunky

All of these problems would go away if only we had a way to return null from our ComWrappers.CreateObject without throwing.

API Proposal

 namespace System.Runtime.InteropServices;

 public abstract class ComWrappers
 {
     public object GetOrCreateObjectForComInstance(IntPtr externalComObject, CreateObjectFlags flags);
+    public bool TryGetOrCreateObjectForComInstance(IntPtr externalComObject, CreateObjectFlags flags, [NotNullWhen(true)] out object? obj);
 }

This API would allow us to completely fix all issues mentioned above, in the most efficient way possible. Our ComWrappers would be able to simply create the right "unboxed" object where appropriate, stash it in some [ThreadStatic] field, and return null. Then our caller would check for null, and if so, grab the "standalone" object from that field, and return that to callers. Everything else would remain the same. We just need to be able to not throw that exception here.

Suggesting the Try pattern here as it's well established. Other names/signatures would be fine though, eg.:

  • Returning object? directly
  • Some other name, eg. GetOrCreateObjectForComInstanceOrDefault
  • etc.

Basically as long as the semantics is as described, we don't have a strong preference here.

API Usage

// In our CsWinRT marshaller
if (!WindowsRuntimeComWrappers.Instance.TryGetOrCreateObjectForComInstance(unk, flags, out object? obj))
{
    obj = UnboxedValue; // Grab the unboxed value from our '[ThreadStatic]' instead
}

// In WindowsRuntimeComWrappers
protected override object CreateObject(IntPtr externalComObject, CreateObjectFlags flags)
{
    if (CheckIfShouldCreateAnRcw(...))
    {
        return rcw;
    }

    // Otherwise, unbox directly
    UnboxedValue = UnboxTheValue(...);
    return null;
}

Alternative Designs

Something like suggested in #113581. This however would not be ideal in this scenario:

  • It'd mean we'd be performing multiple lookups in ComWrappers every single time, which we'd like to avoid
  • It'd also mean ComWrappers would need to do a handful of QIs multiple times as well (to get the object identity, twice)
  • It'd force us to split our logic to handle marshalling in multiple places across CsWinRT, rather than keeping it in CreateObject

Risks

None. It's just the Try version of an existing API, following a well established pattern (don't throw, and return false/null).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions