Description
Background and motivation
As part of our COM interop source generator work, we decided to start with implementing support first for the general concept of virtual function tables. Many native APIs, including JNI, MSQuic, and COM are either implemented or presented to users using tables of function pointers, either explicitly like MSQuic or implicitly through abstract member functions like JNI or COM. Providing this source generator would enable developers to use native APIs like these ones with user-friendly types using the new source-generated marshalling model instead of being forced to use the built-in marshalling model with Marshal.GetDelegateForFunctionPointer
to manually marshal back a virtual method table or dropping down to manual marshalling with function pointers to get decent performance.
This source generator would enable generating code to call an unmanaged API projected to a managed interface and call a managed interface projected to an unmanaged table of function pointers.
We plan on providing guidance directing users to use these APIs to override behavior from the COM source generator (they will integrate cleanly)
API Proposal
These APIs fall into two categories:
- APIs that allow developers to define managed APIs that represent unmanaged virtual method tables (such as COM, JNI, MsQuic).
- APIs that represent concepts that the COM source generator could use as building blocks that are also designed to be seamlessly integrated with the APIs in the first category.
// Types that are only needed for the VTable source generator or to provide abstract concepts that the COM generator would use under the hood.
// These are types that we can exclude from the API proposals and either inline into the generated code, provide as file-scoped types, or not provide publicly (indicated by comments on each type).
using System.Numerics;
namespace System.Runtime.InteropServices.Marshalling;
/// <summary>
/// A factory to create an unmanaged "this pointer" from a managed object and to get a managed object from an unmanaged "this pointer".
/// </summary>
/// <remarks>
/// This interface would be used by the VTable source generator to enable users to indicate how to get the managed object from the "this pointer".
/// We can hard-code the ComWrappers logic here if we don't want to ship this interface.
/// </remarks>
public unsafe interface IUnmanagedObjectUnwrapper
{
/// <summary>
/// Get the object wrapped by <paramref name="ptr"/>.
/// </summary>
/// <param name="ptr">A an unmanaged "this pointer".</param>
/// <returns>The object wrapped by <paramref name="ptr"/>.</returns>
public static abstract object GetObjectForUnmanagedWrapper(void* ptr);
}
// This attribute provides the mechanism for the VTable source generator to know which type to use to get the managed object
// from the unmanaged "this" pointer. If we decide to not expose VirtualMethodIndexAttribute, we don't need to expose this.
[AttributeUsage(AttributeTargets.Interface)]
public class UnmanagedObjectUnwrapperAttribute<TMapper> : Attribute
where TMapper : IUnmanagedObjectUnwrapper
{
}
// This type implements the logic to get the managed object from the unmanaged "this" pointer.
// If we decide to not expose the VTable source generator, we don't need to expose this and we can just inline the logic
// into the generated code in the source generator.
public sealed unsafe class ComWrappersUnwrapper : IUnmanagedObjectUnwrapper
{
public static object GetObjectForUnmanagedWrapper(void* ptr)
{
return ComWrappers.ComInterfaceDispatch.GetInstance<object>((ComWrappers.ComInterfaceDispatch*)ptr);
}
}
/// <summary>
/// Marshals an exception object to the value of its <see cref="Exception.HResult"/> converted to <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The unmanaged type to convert the HResult to.</typeparam>
/// <remarks>
/// This type is used by the COM source generator to enable marshalling exceptions to the HResult of the exception.
/// We can skip the exposing the exception marshallers if we decide to not expose the VTable source generator.
/// In that case, we'd hard-code the implementations of these marshallers into the COM source generator.
/// </remarks>
[CustomMarshaller(typeof(Exception), MarshalMode.UnmanagedToManagedOut, typeof(ExceptionHResultMarshaller<>))]
public static class ExceptionHResultMarshaller<T>
where T : unmanaged, INumber<T>
{
/// <summary>
/// Marshals an exception object to the value of its <see cref="Exception.HResult"/> converted to <typeparamref name="T"/>.
/// </summary>
/// <param name="e">The exception.</param>
/// <returns>The HResult of the exception, converted to <typeparamref name="T"/>.</returns>
public static T ConvertToUnmanaged(Exception e);
}
[CustomMarshaller(typeof(Exception), MarshalMode.UnmanagedToManagedOut, typeof(ExceptionNaNMarshaller<>))]
public static class ExceptionNaNMarshaller<T>
where T : unmanaged, IFloatingPointIeee754<T>
{
/// <summary>
/// Marshals an exception object to <see cref="T.NaN"/>.
/// </summary>
/// <param name="e">The exception.</param>
/// <returns><typeparamref name="T.NaN"/>.</returns>
public static T ConvertToUnmanaged(Exception e);
}
[CustomMarshaller(typeof(Exception), MarshalMode.UnmanagedToManagedOut, typeof(ExceptionDefaultMarshaller<>))]
public static class ExceptionDefaultMarshaller<T>
where T : unmanaged
{
/// <summary>
/// Marshals an exception object to the default value of <typeparamref name="T"/>.
/// </summary>
/// <param name="e">The exception.</param>
/// <returns>The default value of <typeparamref name="T"/>.</returns>
public static T ConvertToUnmanaged(Exception e);
}
[CustomMarshaller(typeof(Exception), MarshalMode.UnmanagedToManagedOut, typeof(SwallowExceptionMarshaller))]
public static class SwallowExceptionMarshaller
{
/// <summary>
/// Swallows the exception.
/// </summary>
/// <param name="e">The exception.</param>
public static void ConvertToUnmanaged(Exception e);
}
public enum ExceptionMarshalling
{
Custom = 0,
Com = 1
}
public enum MarshalDirection
{
ManagedToUnmanaged = 0,
UnmanagedToManaged = 1,
Bidirectional = 2
}
// This is the trigger attribute for the VTable source generator.
// If we decide we want to only expose the COM source generator, then we would keep this attribute internal.
// The current plan is to use this attribute to provide the "don't use the defaults, use this custom logic" options
// for the COM source generator, so if we decide to not expose this, we should provide a different mechanism.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class VirtualMethodIndexAttribute : Attribute
{
public VirtualMethodIndexAttribute(int index);
public int Index { get; }
public bool ImplicitThisParameter { get; set; } = true;
/// <summary>
/// Gets or sets how to marshal string arguments to the method.
/// </summary>
/// <remarks>
/// If this field is set to a value other than <see cref="StringMarshalling.Custom" />,
/// <see cref="StringMarshallingCustomType" /> must not be specified.
/// </remarks>
public StringMarshalling StringMarshalling { get; set; }
/// <summary>
/// Gets or sets the <see cref="Type"/> used to control how string arguments to the method are marshalled.
/// </summary>
/// <remarks>
/// If this field is specified, <see cref="StringMarshalling" /> must not be specified
/// or must be set to <see cref="StringMarshalling.Custom" />.
/// </remarks>
public Type? StringMarshallingCustomType { get; set; }
/// <summary>
/// Gets or sets whether the callee sets an error (SetLastError on Windows or errno
/// on other platforms) before returning from the attributed method.
/// </summary>
public bool SetLastError { get; set; }
public MarshalDirection Direction { get; set; } = MarshalDirection.Bidirectional;
public ExceptionMarshalling ExceptionMarshalling { get; set; }
/// <summary>
/// Gets or sets the <see cref="Type"/> used to control how an exception is marshalled to the return value.
/// </summary>
/// <remarks>
/// If this field is specified, <see cref="ExceptionMarshalling" /> must not be specified
/// or must be set to <see cref="ExceptionMarshalling.Custom" />.
/// </remarks>
public Type? ExceptionMarshallingType { get; set; }
}
API Usage
[UnmanagedObjectUnwrapper<MyObjectUnwrapper>]
partial interface INativeAPI : IUnmanagedInterfaceType
{
[VirtualMethodIndex(0)]
void Method(string param);
}
[UnmanagedObjectUnwrapper<MyObjectUnwrapper>]
unsafe partial interface INativeAPI2 : IUnmanagedInterfaceType
{
private static void* _table;
public static void* IUnmanagedInterface.VirtualMethodTableManagedImplementation => _table;
static INativeAPI2()
{
_table = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(INativeAPI2), sizeof(void*));
Native.FillManagedVirtualMethodTableImplementation(_table);
}
[VirtualMethodIndex(0)]
void Method(string param);
}
class NativeAPI : IUnmanagedVirtualMethodTableProvider, INativeAPI.Native
{
private void* _this;
public NativeAPI(void* thisPtr) { _this = thisPtr; }
VirtualMethodTableInfo IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableInfoForKey(Type type)
{
Debug.Assert(type == typeof(INativeAPI));
return new(_this, *(void***)_this);
}
}
sealed unsafe class MyObjectUnwrapper : IUnmanagedObjectUnwrapper
{
public static object GetObjectForUnmanagedWrapper(void* ptr)
{
throw new NotImplementedException();
}
}
Generated code shape:
partial interface INativeAPI
{
public static void* IUnmanagedInterface.VirtualMethodTableManagedImplementation => /* implementation */ throw null;
internal partial interface Native : INativeAPI
{
// DIM implementations for every method in INativeAPI with a [VirtualMethodIndexAttribute] attribute.
}
}
partial interface INativeAPI2
{
internal partial interface Native : INativeAPI2
{
// DIM implementations for every method in INativeAPI with a [VirtualMethodIndexAttribute] attribute.
// Provided only when the user provides their own implementation of IUnmanagedInterface.VirtualMethodTableManagedImplementation.
// It's extremely annoying to have to know the unmanaged function pointer signatures to take the address of the
// implemented stubs, so provide this method to fill the table so users don't need to.
internal static void FillManagedVirtualMethodTableImplementation(void* vtable) {}
}
}
Original API Proposal (kept for history to match to comments)
namespace System.Runtime.InteropServices.Marshalling;
/// <summary>
/// Information about a virtual method table and the unmanaged instance pointer.
/// </summary>
public readonly ref struct VirtualMethodTableInfo
{
/// <summary>
/// Construct a <see cref="VirtualMethodTableInfo"/> from a given instance pointer and table memory.
/// </summary>
/// <param name="thisPointer">The pointer to the instance.</param>
/// <param name="virtualMethodTable">The block of memory that represents the virtual method table.</param>
public VirtualMethodTableInfo(IntPtr thisPointer, ReadOnlySpan<IntPtr> virtualMethodTable)
{
ThisPointer = thisPointer;
VirtualMethodTable = virtualMethodTable;
}
/// <summary>
/// The unmanaged instance pointer
/// </summary>
public IntPtr ThisPointer { get; }
/// <summary>
/// The virtual method table.
/// </summary>
public ReadOnlySpan<IntPtr> VirtualMethodTable { get; }
/// <summary>
/// Deconstruct this structure into its two fields.
/// </summary>
/// <param name="thisPointer">The <see cref="ThisPointer"/> result</param>
/// <param name="virtualMethodTable">The <see cref="VirtualMethodTable"/> result</param>
public void Deconstruct(out IntPtr thisPointer, out ReadOnlySpan<IntPtr> virtualMethodTable)
{
thisPointer = ThisPointer;
virtualMethodTable = VirtualMethodTable;
}
}
/// <summary>
/// This interface allows an object to provide information about a virtual method table for a managed interface that implements <see cref="IUnmanagedInterfaceType{TInterface}"/> to enable invoking methods in the virtual method table.
/// </summary>
/// <typeparam name="T">The type to use to represent the the identity of the unmanaged type.</typeparam>
public unsafe interface IUnmanagedVirtualMethodTableProvider
{
/// <summary>
/// Get the information about the virtual method table for a given unmanaged interface type represented by <paramref name="type"/>.
/// </summary>
/// <param name="type">The managed type for the unmanaged interface.</param>
/// <returns>The virtual method table information for the unmanaged interface.</returns>
protected VirtualMethodTableInfo GetVirtualMethodTableInfoForKey(Type type);
/// <summary>
/// Get the information about the virtual method table for the given unmanaged interface type.
/// </summary>
/// <typeparam name="TUnmanagedInterfaceType">The managed interface type that represents the unmanaged interface.</typeparam>
/// <returns>The virtual method table information for the unmanaged interface.</returns>
public sealed VirtualMethodTableInfo GetVirtualMethodTableInfoForKey<TUnmanagedInterfaceType>()
where TUnmanagedInterfaceType : IUnmanagedInterfaceType<TUnmanagedInterfaceType>
{
return GetVirtualMethodTableInfoForKey(typeof(TUnmanagedInterfaceType));
}
/// <summary>
/// Get the length of the virtual method table for the given unmanaged interface type.
/// </summary>
/// <typeparam name="TUnmanagedInterfaceType">The managed interface type that represents the unmanaged interface.</typeparam>
/// <returns>The length of the virtual method table for the unmanaged interface.</returns>
public static int GetVirtualMethodTableLength<TUnmanagedInterfaceType>()
where TUnmanagedInterfaceType : IUnmanagedInterfaceType<TUnmanagedInterfaceType>
{
return TUnmanagedInterfaceType.VirtualMethodTableLength;
}
/// <summary>
/// Get a pointer to the virtual method table of managed implementations of the unmanaged interface type.
/// </summary>
/// <typeparam name="TUnmanagedInterfaceType">The managed interface type that represents the unmanaged interface.</typeparam>
/// <returns>A pointer to the virtual method table of managed implementations of the unmanaged interface type</returns>
public static void* GetVirtualMethodTableManagedImplementation<TUnmanagedInterfaceType>()
where TUnmanagedInterfaceType : IUnmanagedInterfaceType<TUnmanagedInterfaceType>
{
return TUnmanagedInterfaceType.VirtualMethodTableManagedImplementation;
}
/// <summary>
/// Get a pointer that wraps a managed implementation of an unmanaged interface that can be passed to unmanaged code.
/// </summary>
/// <typeparam name="TUnmanagedInterfaceType">The managed type that represents the unmanaged interface.</typeparam>
/// <param name="obj">The managed object that implements the unmanaged interface.</param>
/// <returns>A pointer-sized value that can be passed to unmanaged code that represents <paramref name="obj"/></returns>
public static void* GetUnmanagedWrapperForObject<TUnmanagedInterfaceType>(TUnmanagedInterfaceType obj)
where TUnmanagedInterfaceType : IUnmanagedInterfaceType<TUnmanagedInterfaceType>
{
return TUnmanagedInterfaceType.GetUnmanagedWrapperForObject(obj);
}
/// <summary>
/// Get the object wrapped by <paramref name="ptr"/>.
/// </summary>
/// <typeparam name="TUnmanagedInterfaceType">The managed type that represents the unmanaged interface.</typeparam>
/// <param name="ptr">A pointer-sized value returned by <see cref="GetUnmanagedWrapperForObject{TUnmanagedInterfaceType}(TUnmanagedInterfaceType)"/> or <see cref="IUnmanagedInterfaceType{TInterface, TKey}.GetUnmanagedWrapperForObject(TInterface)"/>.</param>
/// <returns>The object wrapped by <paramref name="ptr"/>.</returns>
public static TUnmanagedInterfaceType GetObjectForUnmanagedWrapper<TUnmanagedInterfaceType>(void* ptr)
where TUnmanagedInterfaceType : IUnmanagedInterfaceType<TUnmanagedInterfaceType>
{
return TUnmanagedInterfaceType.GetObjectForUnmanagedWrapper(ptr);
}
}
/// <summary>
/// This interface allows another interface to define that it represents a managed projection of an unmanaged interface from some unmanaged type system.
/// </summary>
/// <typeparam name="TInterface">The managed interface.</typeparam>
/// <typeparam name="TKey">The type of a value that can represent types from the corresponding unmanaged type system.</typeparam>
public unsafe interface IUnmanagedInterfaceType<TInterface>
where TInterface : IUnmanagedInterfaceType<TInterface>
{
/// <summary>
/// Get the length of the virtual method table for the given unmanaged interface type.
/// </summary>
/// <returns>The length of the virtual method table for the unmanaged interface.</returns>
public static abstract int VirtualMethodTableLength { get; }
/// <summary>
/// Get a pointer to the virtual method table of managed implementations of the unmanaged interface type.
/// </summary>
/// <returns>A pointer to the virtual method table of managed implementations of the unmanaged interface type</returns>
public static abstract void* VirtualMethodTableManagedImplementation { get; }
/// <summary>
/// Get a pointer that wraps a managed implementation of an unmanaged interface that can be passed to unmanaged code.
/// </summary>
/// <param name="obj">The managed object that implements the unmanaged interface.</param>
/// <returns>A pointer-sized value that can be passed to unmanaged code that represents <paramref name="obj"/></returns>
public static abstract void* GetUnmanagedWrapperForObject(TInterface obj);
/// <summary>
/// Get the object wrapped by <paramref name="ptr"/>.
/// </summary>
/// <param name="ptr">A pointer-sized value returned by <see cref="IUnmanagedVirtualMethodTableProvider{TKey}.GetUnmanagedWrapperForObject{IUnmanagedInterfaceType{TInterface, TKey}}(IUnmanagedInterfaceType{TInterface, TKey})"/> or <see cref="GetUnmanagedWrapperForObject(TInterface)"/>.</param>
/// <returns>The object wrapped by <paramref name="ptr"/>.</returns>
public static abstract TInterface GetObjectForUnmanagedWrapper(void* ptr);
}
API Usage
partial interface INativeAPI : IUnmanagedInterfaceType<INativeAPI>
{
static int IUnmanagedInterfaceType<INativeAPI>.VirtualMethodTableLength => 1;
private static void** s_vtable = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(INativeAPI), sizeof(void*) * IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableLength<INativeAPI>());
static void* IUnmanagedInterfaceType<INativeAPI>.VirtualMethodTableManagedImplementation
{
get
{
if (s_vtable[0] == null)
{
Native.PopulateUnmanagedVirtualMethodTable(new Span<IntPtr>(s_vtable, IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableLength<INativeAPI>()));
}
return s_vtable;
}
}
static void* IUnmanagedInterfaceType<INativeAPI>.GetUnmanagedWrapperForObject(INativeAPI api) => throw new NotImplementedException();
static INativeAPI IUnmanagedInterfaceType<INativeAPI>.GetObjectForUnmanagedWrapper(void* ptr) => throw new NotImplementedException();
static INativeAPI()
{
}
[VirtualMethodIndex(0)]
void Method(string param);
}
class NativeAPI : IUnmanagedVirtualMethodTableProvider, INativeAPI.Native
{
private void* _this;
public NativeAPI(void* thisPtr) { _this = thisPtr; }
VirtualMethodTableInfo IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableInfoForKey(Type type)
{
Debug.Assert(type == typeof(INativeAPI));
return new((IntPtr)_this, new ReadOnlySpan<IntPtr>(*(void**)_this, IUnmanagedVirtualMethodTableProvider.GetVirtualMethodTableLength<INativeAPI>()));
}
}
Generated code shape:
partial interface INativeAPI
{
internal partial interface Native : INativeAPI
{
// DIM implementations for every method in INativeAPI with a [VirtualMethodIndexAttribute] attribute.
internal static PopulateUnmanagedVirtualMethodTable(Span<nint> vtable) { /* fill the vtable with function pointers for the unmanaged->managed stubs */
}
}
Alternative Designs
Risks
The DIM-implemented methods aren't visible when using the implementing types directly; they're only visible through the interface. As a result, the user experience is a little weird for the cases where only one interface is implemented, as the interface methods can only be called on an object wrapping the native API through the interface, not through the wrapping class.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status