dotnet / runtime

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

[API Proposal]: System.Runtime.InteropServices.Marshalling.DelegateMarshaller for delegate #108738

Open Gaoyifei1011 opened 2 weeks ago

Gaoyifei1011 commented 2 weeks ago

Background and motivation

System.Runtime.InteropServices.Marshalling.DelegateMarshaller for delegate

The System.Runtime.InteropServices.Marshalling namespace already contains a number of XXXMarshaller classes for P/Invoke source generation and COM source generation, which play an important role. However, regarding the delegate delegate type, we need to manually get the pointer of the delegate type or convert the pointer to the delegate type (method: Marshal. GetFunctionPointerForDelegate() and Marshal. GetDelegateForFunctionPointer())。 So hopefully the DOTNET team will provide a DelegateMarshaller type that can automatically handle delegate-type marshalling, similar to UTF16StringMarshaller That's it.

----------------------------------------------

目前在 System.Runtime.InteropServices.Marshalling 命名空间中已经包含了大量的 XXXMarshaller 类用于 P/Invoke 源生成和 COM 源生成,这些类起到了重要作用。然而有关 delegate 委托类型,需要我们手动获取 delegate 类型的指针或者将指针转换为委托类型(方法:Marshal.GetFunctionPointerForDelegate()Marshal.GetDelegateForFunctionPointer())。所以希望 DOTNET 团队能提供一个 DelegateMarshaller 类型,能自动处理 delegate 类型封送,类似 UTF16StringMarshaller 那样。

API Proposal

namespace System.Runtime.InteropServices.Marshalling
{
    [CLSCompliant(false)]
    [CustomMarshaller(typeof(Delegate), MarshalMode.Default, typeof(DelegateMarshaller))]
    public static unsafe class DelegateMarshaller
    {
        /// <summary>
        /// Converts a delegate to an unmanaged version.
        /// </summary>
        /// <param name="managed">The managed delegate to convert.</param>
        /// <returns>An unmanaged delegate.</returns>
        public static void* ConvertToUnmanaged(Delegate? managed);

        /// <summary>
        /// Converts an unmanaged delegate to a managed version.
        /// </summary>
        /// <param name="unmanaged">The unmanaged delegate to convert.</param>
        /// <returns>A managed delegate.</returns>
        public static Delegate? ConvertToManaged(void* unmanaged);
    }

    [CLSCompliant(false)]
    [CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder), MarshalMode.Default, typeof(DelegateMarshaller<>))]
    public static unsafe class DelegateMarshaller<TDelegate> where TDelegate : notnull
    {
        /// <summary>
        /// Converts a delegate to an unmanaged version.
        /// </summary>
        /// <param name="managed">The managed delegate to convert.</param>
        /// <returns>An unmanaged delegate.</returns>
        public static void* ConvertToUnmanaged(TDelegate? managed);

        /// <summary>
        /// Converts an unmanaged delegate to a managed version.
        /// </summary>
        /// <param name="unmanaged">The unmanaged delegate to convert.</param>
        /// <returns>A managed delegate.</returns>
        public static TDelegate? ConvertToManaged(void* unmanaged);
    }
}

API Usage

Use for LibraryImport (P/Invoke source generation) and COMWrappers (COM source generation)

Alternative Designs

None

Risks

None

AaronRobinsonMSFT commented 2 weeks ago

@Gaoyifei1011 Thanks for the suggestion. I know we discussed this when we were bringing up the LibraryImport generator. I thought we baked Delegate support into the system. Meaning there is no need for a custom marshaller, the source generator will just do the right thing, like the built-in case. See below:

https://github.com/dotnet/runtime/blob/96968171150e154a2657d6fadca8b75bea9022c4/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/DelegateMarshaller.cs#L13

Tests:

https://github.com/dotnet/runtime/blob/96968171150e154a2657d6fadca8b75bea9022c4/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.Tests/DelegateTests.cs#L10-L23

Can you elaborate on what specifically you need from a Delegate marshaller?

Gaoyifei1011 commented 2 weeks ago

@Gaoyifei1011 Thanks for the suggestion. I know we discussed this when we were bringing up the LibraryImport generator. I thought we baked Delegate support into the system. Meaning there is no need for a custom marshaller, the source generator will just do the right thing, like the built-in case. See below:

runtime/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/DelegateMarshaller.cs

Line 13 in 9696817

public sealed class DelegateMarshaller : IUnboundMarshallingGenerator Tests:

runtime/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.Tests/DelegateTests.cs

Lines 10 to 23 in 9696817

partial class NativeExportsNE { public delegate void VoidVoid();

 [LibraryImport(NativeExportsNE_Binary, EntryPoint = "invoke_callback_after_gc")] 
 public static partial void InvokeAfterGC(VoidVoid cb); 

 public delegate int IntIntInt(int a, int b); 

 [LibraryImport(NativeExportsNE_Binary, EntryPoint = "invoke_callback_blittable_args")] 
 public static partial int InvokeWithBlittableArgument(IntIntInt cb, int a, int b); 

}

public class DelegateTests Can you elaborate on what specifically you need from a Delegate marshaller?

Image https://learn.microsoft.com/zh-cn/dotnet/standard/native-interop/disabled-marshalling

I always thought that there was no default support yet and that I needed to manually convert to an IntPtr pointer.


我一直以为还没有提供默认支持,需要手动转换成IntPtr 指针。

Gaoyifei1011 commented 2 weeks ago

@Gaoyifei1011 Thanks for the suggestion. I know we discussed this when we were bringing up the LibraryImport generator. I thought we baked Delegate support into the system. Meaning there is no need for a custom marshaller, the source generator will just do the right thing, like the built-in case. See below:

runtime/src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/DelegateMarshaller.cs

Line 13 in 9696817

public sealed class DelegateMarshaller : IUnboundMarshallingGenerator Tests:

runtime/src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.Tests/DelegateTests.cs

Lines 10 to 23 in 9696817

partial class NativeExportsNE { public delegate void VoidVoid();

 [LibraryImport(NativeExportsNE_Binary, EntryPoint = "invoke_callback_after_gc")] 
 public static partial void InvokeAfterGC(VoidVoid cb); 

 public delegate int IntIntInt(int a, int b); 

 [LibraryImport(NativeExportsNE_Binary, EntryPoint = "invoke_callback_blittable_args")] 
 public static partial int InvokeWithBlittableArgument(IntIntInt cb, int a, int b); 

}

public class DelegateTests Can you elaborate on what specifically you need from a Delegate marshaller?

Image Image

So do the pointers of the struct support?

AaronRobinsonMSFT commented 2 weeks ago

So do the pointers of the struct support?

Ah. Field marshalling requires using the old pattern since we don't expose the marshaller. The problem with field marshalling is how complicated it gets. For example, the proposal has a bug in it because the Delegate instance needs a GC.KeepAlive() during the call. The marshaller must be stateful to be correct.

Gaoyifei1011 commented 2 weeks ago

So do the pointers of the struct support?

Ah. Field marshalling requires using the old pattern since we don't expose the marshaller. The problem with field marshalling is how complicated it gets. For example, the proposal has a bug in it because the instance needs a during the call. The marshaller must be stateful to be correct.Delegate``GC.KeepAlive()

Well, thank you very much for the answer

AaronRobinsonMSFT commented 2 weeks ago

A correct Delegate marshaller will need state to keep the Delegate instance alive. See the code that is emitted by the LibraryImport source generator for what would need to happen.