dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.07k stars 4.69k forks source link

[API Proposal]: `Delegate.UnsafeCreateFromPointer` #94977

Open MichalPetryka opened 10 months ago

MichalPetryka commented 10 months ago

Background and motivation

In some cases a programmer might have a function pointer to a managed method while needing to have a delegate to it. One could then wrap such function pointer in a capturing delegate but that incurs both the cost of an additional indirection and looses reflection info for the method. Creating a delegate with a pointer is already possible in IL on CoreCLR, but IIRC NativeAOT doesn't fully implement the constructor for it. It'd be preferable however to have such possibility be exposed in C#.

Exposing this would make it possible to convert between delegate types since you could create a delegate of a new type with a pointer from the old delegate.

This would also help with the language not exposing a way to call instance function pointers today.

This API is unsafe since the runtime currently has no way to verify if a function pointer points to a valid managed method and crashes on such pointer (see #85197).

API Proposal

namespace System;

public class Delegate
{
    public static TDelegate UnsafeCreateFromPointer<TDelegate>(void* pointer, object target = null);
}

API Usage

public class C
{
    public void A() {}
    public static void B() {}
}

delegate*<C, void> ptrA = (delegate*<C, void>)typeof(C).GetMethod("A").MethodHandle.GetFunctionPointer(); // this should have `explicitthis` but C# can't express this
delegate*<void> ptrB = &C.B;

Console.WriteLine(Delegate.UnsafeCreateFromPointer<Action<C>>(ptrA).Method); // prints C.A
Console.WriteLine(Delegate.UnsafeCreateFromPointer<Action>(ptrA, new C()).Method); // prints C.A
Console.WriteLine(Delegate.UnsafeCreateFromPointer<Action>(ptrB).Method); // prints C.B

Alternative Designs

Exposing the hidden pointer constructor on delegates, this however would not suggest that such operation is unsafe.

Risks

Users crashing the runtime by providing invalid function pointers.

ghost commented 10 months ago

Tagging subscribers to this area: @dotnet/area-system-runtime See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation In some cases a programmer might have a function pointer to a managed method while needing to have a delegate to it. One could then wrap such function pointer in a capturing delegate but that incurs both the cost of an additional indirection and looses reflection info for the method. Creating a delegate with a pointer is already possible in IL on CoreCLR, but IIRC NativeAOT doesn't fully implement the constructor for it. It'd be preferable however to have such possibility be exposed in C#. Exposing this would make it possible to convert between delegate types since you could create a delegate of a new type with a pointer from the old delegate. This would also help with the language not exposing a way to call instance function pointers today. This API is unsafe since the runtime currently has no way to verify if a function pointer points to a valid managed method and crashes on such pointer (see #85197). ### API Proposal ```csharp namespace System; public class Delegate { public static TDelegate UnsafeCreateFromPointer(void* pointer, object target = null); } ``` ### API Usage ```csharp public class C { public void A() {} public static void B() {} } delegate* ptrA = (delegate*)typeof(C).GetMethod("A").MethodHandle.GetFunctionPointer(); // this should have `explicitthis` but C# can't express this delegate* ptrB = &C.B; Console.WriteLine(Delegate.UnsafeCreateFromPointer>(ptrA).Method); // prints C.A Console.WriteLine(Delegate.UnsafeCreateFromPointer(ptrA, new C()).Method); // prints C.A Console.WriteLine(Delegate.UnsafeCreateFromPointer(ptrB).Method); // prints C.B ``` ### Alternative Designs Exposing the hidden pointer constructor on delegates, this however would not suggest that such operation is unsafe. ### Risks Users crashing the runtime by providing invalid function pointers.
Author: MichalPetryka
Assignees: -
Labels: `api-suggestion`, `area-System.Runtime`, `untriaged`
Milestone: -
jkotas commented 10 months ago

Exposing the hidden pointer constructor on delegates, this however would not suggest that such operation is unsafe.

The hidden constructor is exposed via UnsafeAccessor that makes it clear that it is an unsafe operation. For example:

using System.Runtime.CompilerServices;

unsafe
{
    IntPtr p = (IntPtr)(delegate* <void>)&Hello;
    MyDelegate d = ConstructMyDelegate(null, p);
    d();
}

[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
extern static MyDelegate ConstructMyDelegate(object o, IntPtr p);

static void Hello()
{
    Console.WriteLine("Hello world");
}

delegate void MyDelegate();

NativeAOT doesn't fully implement the constructor for it.

This constructor is reflection heavy operation. It needs to convert the pointer back to MethodInfo, find the signature of the MethodInfo, compare it with the signature of the delegate, and then compute the set of wrappers and thunks to convert the call with one signature to the call with the other signature. It can be all implemented in native AOT, but I suspect that it would require preserving more metadata.

MichalStrehovsky commented 10 months ago

Yeah, we're unlikely to implement delegate constructor methods in native AOT. Doing so would require considering all address-taken methods to be visible from reflection (because this operation would fail without the metadata). We're already forced to do that for methods that are targets of delegates and that's not great either. Cc @Sergio0694

MichalPetryka commented 10 months ago

Could ILCompiler scan usages of delegate constructors and only keep the required metadata for methods that match the signature of a delegate created from an unidentified pointer? This way this wouldn't bring more metadata if such delegate creation isn't used.

jkotas commented 10 months ago

It can, but it would be a very complicate analysis for a very niche scenario.

If you want to do things like convert delegates, reflection works as good as the proposed method without any additional analysis.