- 
                Notifications
    
You must be signed in to change notification settings  - Fork 5.2k
 
Description
Background and motivation
The VARIANT type is a discriminated union data structure that is common in COM interop. The built-in system has various APIs off of Marshal that help with marshalling of VARIANT (for example, Marshal.GetObjectForNativeVariant()). There is also support in the built-in COM Interop for marshalling as a VARIANT (for example, [MarshalAs(UnmanagedType.Struct)] object a). This proposal is for add a marshaller for consumers that helps bridge the gap. Precise support what this type is permitted to hold will initially be limited to primitives and IUnknown types. However this can change but will largely be driven by user need/feedback.
The primary goal would be to support the WinForms effort to become AOT compatible.
Support for this would require usage of MarshalUsingAttribute as opposed to existing usages with MarshalAsAttribute in the example above. This decision is being made since current usages of the MarshalAs pattern are too permissive and we are attempting to narrow when usage is supported.
API Proposal (Updated 21-09-2023)
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace System.Runtime.InteropServices.Marshalling;
[StructLayout(LayoutKind.Explicit)]
public struct OleVariant : IDisposable
{
    // private fields to match the layout of VARIANT
    public OleVariant();
    public void Dispose();
    // Determine the VarEnum for the variant based on T.
    // Respects the System.Runtime.InteropServices.*Wrapper types for specifying the variant type.
    // We will not allow System.Object as T (we will throw an InvalidOperationException).
    // If a System.Runtime.InteropServices.*Wrapper type that corresponds to a COM interface is specified,
    // the following behavior occurs:
    // - If Marshal.IsComObject() returns true, call Marshal.GetIUnknown/IDispatchForObject
    // - Otherwise, use ComInterfaceMarshaller<T> to get the interface pointer.
    // Types without a corresponding VarEnum type are not allowed.
    public static OleVariant Create<T>([DisallowNull] T value);
    // Explicitly specify the VarEnum for the variant and the raw value.
    // Throws if T is larger than the VARIANT's data union.
    // Throws for VT_DECIMAL.
    // Throws if T does not match the size of the data union member that vt corresponds to.
    public static OleVariant CreateRaw<T>(VarEnum vt, T rawValue) where T : unmanaged;
    // Create VT_NULL variant
    public static OleVariant Null { get; };
    // Get a .NET object of the specified type that represents the underlying VARIANT value.
    // For COM object-based VARIANTs, uses ComInterfaceMarshaller to create the managed object.
    // If passed one of the System.Runtime.InteropServices.*Wrapper types, the wrapper type will be used
    // if it matches the variant type. Using a wrapper type is not required.
    // If the requested type does not match the underlying type, throws an exception.
    public T As<T>();
    // For cases where the built-in Create/As methods are not sufficient:
    // Expose accessors for the underlying fields of the VARIANT struct
    // but in a limited fashion.
    // This will allow consumers such as WinForms to support VARIANT types
    // that we do not plan to support immediately (like SAFEARRAY).
    public VarEnum VarType { get; }
    // Returns a reference to the data union within the variant.
    // Throws if T is larger than the VARIANT's data union.
    [UnscopedRef]
    public ref T GetRawDataRef<T>() where T : unmanaged;
}
// Let's start with a marshaller for object<->OleVariant as this will cover the majority of cases.
// We can add a generic marshaller for T<->OleVariant later if needed (e.g. for an IDispatch-based generator)
[CustomMarshaller(typeof(object?), MarshalMode.Default, typeof(OleVariantMarshaller))]
// To ensure correct behavior, we will need to update the generator to use the "ref" shape for both of the following cases.
// Is this behavior we want to generalize in an opt-in way? Or should we just special-case this case for now?
[CustomMarshaller(typeof(object?), MarshalMode.UnmanagedToManagedIn, typeof(OleVariantMarshaller.RefPropogate))]
[CustomMarshaller(typeof(object?), MarshalMode.UnmanagedToManagedRef, typeof(OleVariantMarshaller.RefPropogate))]
public static class OleVariantMarshaller
{
    public static OleVariant ConvertToUnmanaged(object? managed);
    public static object? ConvertToManaged(OleVariant unmanaged);
    public static void Free(OleVariant unmanaged);
    public struct RefPropogate
    {
        public void FromUnmanaged(OleVariant unmanaged);
        public void FromManaged(object? managed);
        public OleVariant ToUnmanaged();
        public object? ToManaged();
        public void Free();
    }
}API Usage
OleVariant variant = OleVariant.Create(1.0m);
OleVariant variant2 = OleVariant.Create(new ErrorWrapper(0));
if (variant2.VarType == VarEnum.VT_ERROR)
{
    variant2.GetRawDataRef<int>() = 1;
}
[LibraryImport("Foo")]
public static partial Bar([MarshalUsing(typeof(OleVariantMarshaller))] object x);Alternative Designs
No response
Risks
Minimal. The primary risk is narrowly supporting VARIANT in ways that limit future efforts. The WinForms team has rewritten a majority of the VARIANT support for their purposes so we can learn from them and mitigate much of the risk. Ideally the Interop team will simply take ownership of the WinForms code so the entire .NET ecosystem can benefit.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status