-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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 boxedint
IReference<SomeDelegate>
-> we return aSomeDelegate
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
Labels
Type
Projects
Status