-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
We're currently using Unsafe.As<T>(object)
quite a bit to optimize our messenger types in the MVVM Toolkit (https://aka.ms/mvvmtoolkit/docs), but I've been thinking for a while now that it'd be nice to remove that call to avoid relying on implementation details of the runtime to make things work, and to make the functionality safer in the future. Just to recap on the current setup (I've added a more extensive summary here), we basically have this (simplifying):
// The message handler delegate, whic is generic on the recipient type
public delegate void Handler<in TRecipient>(TRecipient recipient)
where TRecipient : class;
// Message broadcast
object handler = GetTheHandler();
object recipient = GetTheRecipient();
Unsafe.As<Handler<object>>(handler)(recipient); // yehaa! 🤠
Now, this works fine because we're always passing the "right" recipient, so the actual type safety when the delegate is invoked is respected. But, we're still aliasing a reference type with a type that doesn't actually match the one of the target instance, and even though the runtime/GC are fine with this now, it's not really ideal, so to say. It works, though.
As @jkotas mentioned in our previous conversation on this (here),
"The specific use of Unsafe.As that you have highlighted sounds reasonable to me. I understand that the generic constrains are not always expressive enough to do what you need."
Despite there not being a way to do this via the language (it's also arguably a pretty niche feature, though I know I'm not the only one using this), I was thinking it would be nice to at least explore some options to do this in a way that doesn't rely on aliasing reference types with incompatible types and potentially causing GC issues on other runtimes and whatnot.
cc. @GrabYourPitchforks as we've been talking about this quite a lot in the #lowlevel
C# Discord 😄
Related issues:
- Stop hiding function pointer delegate constructor csharplang#4871: this requires a language change and it would also not work when eg. a delegate is wrapping a dynamic method, as those don't allow retrieving the method handle and the function pointer. The proposed approach instead would work with any delegate instance.
- API to change a delegate's type to a signature-compatible different delegate type #45408: this was rejected due to the request to add potentially expensive runtime type checks. That wouldn't be the case here: the proposed API would just ask the runtime to create a delegate of the new type, by copying all the internal fields (so target instance, function pointers including optional stub, etc.). This would also make it so that the proposed approach would work fine when the delegate is generated from a dynamic method, as mentioned above.
API Proposal
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static TDelegate ChangeType<TDelegate>(Delegate del)
where TDelegate : Delegate;
}
}
NOTE: the returned delegate would not just be a new delegate wrapping the input one, but a new instance of the requested type with the same internal state, resulting in the same method/target being used when invoked.
API Usage
// Message registration
ref object storedHandler = ref GetTheHandler();
storedHandler = RuntimeHelpers.ChangeType<Handler<object>>(handler);
// Message broadcast
object handler = GetTheHandler();
object recipient = GetTheRecipient();
((Handler<object>)handler)(recipient);
Same functionality, hopefully negligible performance loss during broadcast (I could also keep using Unsafe.As<T>(object)
there if that variant cast was noticeable, but at least the behavior there would now be safe and no longer aliasing an invalid type), and no longer relying on hacks to make the whole thing work while being fast and without reflection 😄
Risks
Low risk. Changing the delegate type to an invalid type could result in crashes and type safety violations at runtime when the new delegate is invoked. But the API would be in an inherently unsafe type and namespace, so only advanced users knowing what they're doing would ever see the API, let alone actually use them in their code. Besides, you can still do all sorts of ugly things with Unsafe.As<T>(object)
anyway, which is also in the same namespace 🙂