runtime

Support for Marshalling (ReadOnly)Span<T>

As part of the exit criteria for the LibraryImportGenerator experiment, we have decided to introduce support for marshalling System.Span<T> and System.ReadOnlySpan<T> into the LibraryImportGenerator-generated stubs. This document describes design decisions made during the implementation of these marshallers.

NOTE: These design docs are kept for historical purposes. The designs in this file are superseded by the designs in UserTypeMarshallingV2.md.

Design 1: “Intrinsic” support for (ReadOnly)Span<T>

In this design, the default support for (ReadOnly)Span<T> is emitted into the marshalling stub directly and builds on the pattern we enabled for arrays.

Default behavior

This section describes the behavior of the default emitted-in-stub marshallers.

By default, we will marshal Span<T> and ReadOnlySpan<T> similarly to array types. When possible, we will pin the (ReadOnly)Span<T>’s data and pass that down. When it is not possible to do so, we will try stack allocating scratch space for efficiency or allocate native memory with Marshal.AllocCoTaskMem for compatibility with arrays.

To support marshalling from native to managed, we will support the same MarshalAsAttribute properties that arrays support today.

When a (ReadOnly)Span<T> is marshalled from native to managed, we will allocate a managed array and copy the data from the native memory into the array.

Empty spans

We have decided to match the managed semantics of (ReadOnly)Span<T> to provide the smoothest default experience for span users in interop. To assist developers who wish to transition from array parameters to span parameters, we will also provide an in-source marshaller that enables marshalling a span that wraps an empty array as a non-null pointer as an opt-in experience.

Additional proposed in-source marshallers

As part of this design, we would also want to include some in-box marshallers that follow the design laid out in the Struct Marshalling design doc to support some additional scenarios:

Pros/Cons of Design 1

Pros:

Cons:

Design 2: “Out-line” default support with extensions to Native Type Marshalling for Contiguous Collections

An alternative option to fully inlining the stub would be to extend the model described in the Struct Marshalling design doc to have custom support for collection-like types. By extending the model to be built with generic collection types in mind, many of the cons of the first approach would be resolved.

Span marshalling would still be implemented with similar semantics as mentioned above in the Empty Spans section. Additional marshallers would still be provided as mentioned in the Additional proposed in-source marshallers section, but the non-null span marshaller and the no-alloc span marshaller would be able to be used in all cases, not just for empty spans.

Proposed extension to the custom type marshalling design

Introduce a marshaller kind named LinearCollection.

namespace System.Runtime.InteropServices
{
  [AttributeUsage(AttributeTargets.Struct)]
  public sealed class CustomTypeMarshallerAttribute : Attribute
  {
+       /// <summary>
+       /// This type is used as a placeholder for the first generic parameter when generic parameters cannot be used
+       /// to identify the managed type (i.e. when the marshaller type is generic over T and the managed type is T[])
+       /// </summary>
+       public struct GenericPlaceholder
+       {
+       }
  }

  public enum CustomTypeMarshallerKind
  {
      Value,
+     LinearCollection
  }
}

The attribute would be used with a collection type like Span<T> as follows:

[NativeTypeMarshalling(typeof(DefaultSpanMarshaller<>))]
public ref struct Span<T>
{
  ...
}

[CustomTypeMarshaller(typeof(Span<>), CustomTypeMarshallerKind.LinearCollection)]
public ref struct DefaultSpanMarshaller<T>
{
  ...
}

The CustomTypeMarshallerKind.LinearCollection kind is applied to a generic marshaller type with the “LinearCollection marshaller shape” described below.

Supporting generics

Since generic parameters cannot be used in attributes, open generic types will be permitted in the NativeTypeMarshallingAttribute and the CustomTypeMarshallerAttribute as long as they have the same arity as the type with the attribute and generic parameters provided to the type with the attribute can also be used to construct the type passed as a parameter.

If a CustomTypeMarshaller-attributed type is a marshaller for a type for a pointer, an array, or a combination of pointers and arrays, the CustomTypeMarshallerAttribute.GenericPlaceholder type can be used in the place of the first generic parameter of the marshaller type.

For example:

[CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder), Direction = CustomTypeMarshallerDirection.In)]
struct Marshaller<T>
{
    public Marshaller(T managed);
}
[CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder[]), Direction = CustomTypeMarshallerDirection.In)]
struct Marshaller<T>
{
    public Marshaller(T[] managed);
}
[CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder*), Direction = CustomTypeMarshallerDirection.In)]
struct Marshaller<T> where T : unmanaged
{
    public Marshaller(T* managed);
}
[CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder*[]), Direction = CustomTypeMarshallerDirection.In)]
struct Marshaller<T> where T : unmanaged
{
    public Marshaller(T*[] managed);
}

LinearCollection marshaller shape

A generic collection marshaller would be required to have the following shape, in addition to the requirements for marshaller types used with the CustomTypeMarshallerKind.Value shape, excluding the constructors.

[CustomTypeMarshaller(typeof(GenericCollection<, , ,...>), CustomTypeMarshallerKind.LinearCollection)]
public struct GenericContiguousCollectionMarshallerImpl<T, U, V,...>
{
    // this constructor is required if marshalling from native to managed is supported.
    public GenericContiguousCollectionMarshallerImpl(int nativeSizeOfElement);
    // these constructors are required if marshalling from managed to native is supported.
    public GenericContiguousCollectionMarshallerImpl(GenericCollection<T, U, V, ...> collection, int nativeSizeOfElement);
    public GenericContiguousCollectionMarshallerImpl(GenericCollection<T, U, V, ...> collection, Span<byte> stackSpace, int nativeSizeOfElement); // optional

    /// <summary>
    /// A span that points to the memory where the managed values of the collection are stored.
    /// </summary>
    public ReadOnlySpan<TCollectionElement> GetManagedValuesSource();
    /// <summary>
    /// A span that points to the memory where the unmarshalled managed values of the collection should be stored.
    /// </summary>
    public Span<TCollectionElement> GetManagedValuesDestination(int length);
    /// <summary>
    /// A span that points to the memory where the native values of the collection are stored after the native call.
    /// </summary>
    public ReadOnlySpan<byte> GetNativeValuesSource(int length);
    /// <summary>
    /// A span that points to the memory where the native values of the collection should be stored.
    /// </summary>
    public Span<byte> GetNativeValuesDestination();

    // The requirements on the TNative type are the same as when used with `NativeTypeMarshallingAttribute`.
    // The property is required with the generic collection marshalling.
    public TNative ToNativeValue();

    public void FromNativeValue(TNative value);
}

The constructors now require an additional int parameter specifying the native size of a collection element. The collection element type is represented as TCollectionElement above, and can be any type the marshaller defines. As the elements may be marshalled to types with different native sizes than managed, this enables the author of the generic collection marshaller to not need to know how to marshal the elements of the collection, just the collection structure itself.

When the elements of the collection are blittable, the marshaller will emit a block copy of the span GetManagedValuesSource/GetNativeValuesSource to GetNativeValuesDestination/GetManagedValuesDestination. When the elements are not blittable, the marshaller will emit a loop that will marshal the elements of the managed span one at a time and store them in the NativeValueStorage span.

This would enable similar performance metrics as the current support for arrays as well as Design 1’s support for the span types when the element type is blittable.

Providing additional data for collection marshalling

As part of collection marshalling, there needs to be a mechanism for the user to tell the stub code generator how many elements are in the native collection when unmarshalling. For parity with the previous system, there also needs to be a mechanism to describe how to marshal the elements of the collection. This proposal adds the following members to the MarshalUsingAttribute attribute to enable this and other features:


- [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Field)]
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Field, AllowMultiple=true)]
public class MarshalUsingAttribute : Attribute
{
+    public MarshalUsingAttribute() {}
     public MarshalUsingAttribute(Type nativeType) {}
+    public string CountElementName { get; set; }
+    public int ConstantElementCount { get; set; }
+    public int ElementIndirectionLevel { get; set; }
+    public const string ReturnsCountValue = "return-value";
}

The MarshalUsingAttribute will now provide a CountElementName property that will point to a parameter (or a field in a struct-marshalling context) whose value will hold the number of native collection elements, or to the return value if the value of CountElementName is ReturnsCountValue. The ConstantElementCount property allows users to provide a constant collection length.

Open Question: Should combining CountElementName and ConstantElementCount in the same attribute be allowed? With the MarshalAs marshalling, SizeParamIndex and SizeConst can be combined and the resulting size will be paramValue(SizeParamIndex) + SizeConst.

To support supplying information about collection element counts, a parameterless constructor is added to the MarshalUsingAttribute type. The default constructor specifies that the code generator should use the information in the attribute but use the default marshalling rules for the type.

The ElementIndirectionLevel property is added to support supplying marshalling info for element types in a collection. For example, if the user is passing a List<List<Foo>> from managed to native code, they could provide the following attributes to specify marshalling rules for the outer and inner lists and Foo separately:

private static partial void Bar([MarshalUsing(typeof(ListAsArrayMarshaller<List<Foo>>), CountElementName = nameof(count)), MarshalUsing(ConstantElementCount = 10, ElementIndirectionLevel = 1), MarshalUsing(typeof(FooMarshaller), ElementIndirectionLevel = 2)] List<List<Foo>> foos, int count);

Multiple MarshalUsing attributes can only be supplied on the same parameter or return value if the ElementIndirectionLevel property is set to distinct values. One MarshalUsing attribute per parameter or return value can leave the ElementIndirectionLevel property unset. This attribute controls the marshalling of the collection object passed in as the parameter. The sequence of managed types for ElementIndirectionLevel is based on the elements of the ManagedValues span on the collection marshaller of the previous indirection level. For example, for the marshalling info for ElementIndirectionLevel = 1 above, the managed type is the type of the following C# expression: ListAsArrayMarshaller<List<Foo>>.ManagedValues[0].

Alternatively, the MarshalUsingAttribute could provide a Type ElementNativeType { get; set; } property instead of an ElementIndirectionLevel property and support specifying the native type of the element of the collection this way. However, this design would block support for marshalling collections of collections.

Example: Using generic collection marshalling for spans

This design could be used to provide a default marshaller for spans and arrays. Below is an example simple marshaller for Span<T>. This design does not include all possible optimizations, such as stack allocation, for simpilicity of the example.

[CustomTypeMarshaller(typeof(Span<>), CustomTypeMarshallerKind.LinearCollection, Features = CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.TwoStageMarshalling)]
public ref struct SpanMarshaller<T>
{
    private Span<T> managedCollection;

    private int nativeElementSize;

    private IntPtr Value { get; set; }

    public SpanMarshaller(Span<T> collection, int nativeSizeOfElement)
    {
       managedCollection = collection;
       Value = Marshal.AllocCoTaskMem(collection.Length * nativeSizeOfElement);
       nativeElementSize = nativeSizeOfElement;
       nativeElementSize = nativeSizeOfElement;
    }

    public ReadOnlySpan<T> GetManagedValuesSource() => managedCollection;

    public Span<T> GetManagedValuesDestination(int length) => managedCollection = new T[length];

    public unsafe Span<byte> GetNativeValuesDestination() => MemoryMarshal.CreateSpan(ref *(byte*)(Value), managedCollection.Length);

    public unsafe Span<byte> GetNativeValuesSource(int length) => MemoryMarshal.CreateSpan(ref *(byte*)(Value), length);

    public Span<T> ToManaged() => managedCollection;

    public IntPtr ToNativeValue() => Value;

    public void FromNativeValue(IntPtr value) => Value = value;

    public void FreeNative()
    {
      if (Value != IntPtr.Zero)
      {
          Marshal.FreeCoTaskMem(Value);
      }
    }
}

The following example would show the expected stub for the provided signature (assuming that Span<T> has a [NativeMarshalling(typeof(SpanMarshaller<>))] attribute):

struct WrappedInt
{
  private int value;

  public WrappedInt(int w)
  {
     value = w.i;
  }

  public int ToManaged() => value;
}

[LibraryImport("Native")]
[return:MarshalUsing(CountElementName = nameof(length))]
public static partial Span<int> DuplicateValues([MarshalUsing(typeof(WrappedInt), ElementIndirectionLevel = 1)] Span<int> values, int length);

// Generated stub:
public static partial unsafe Span<int> DuplicateValues(Span<int> values, int length)
{
     SpanMarshaller<int> __values_marshaller = new SpanMarshaller<int>(values, sizeof(WrappedInt));
     {
          ReadOnlySpan<int> __values_managedSpan = __values_marshaller.GetManagedValuesSource();
          Span<byte> __values_nativeSpan = __values_marshaller.GetNativeValuesDestination();
          for (int i = 0; i < __values_managedSpan.Length; ++i)
          {
              WrappedInt native = new WrappedInt(__values_managedSpan[i]);
              MemoryMarshal.Write(__values_nativeSpan.Slice(sizeof(WrappedInt) * i), ref native);
          }
     }

     IntPtr __retVal_native = __PInvoke__(__values_marshaller.ToNativeValue(), length);
     SpanMarshaller<int> __retVal_marshaller = new();
     __retVal_marshaller.FromNativeValue(__retVal_native);
     MemoryMarshal.Cast<byte, int>(__retVal_marshaller.GetNativeValuesSource(length)).CopyTo(__retVal_marshaller.GetManagedValuesDestination(length));
     return __retVal_marshaller.ToManaged();

     [DllImport("Native", EntryPoint="DuplicateValues")]
     static extern IntPtr __PInvoke__(IntPtr values, int length);
}

This design could also be applied to support the built-in array marshalling if it is desired to move that marshalling out of the stub and into shared code.

Future extension to the above model: Non-contiguous collection support

If a managed or native representation of a collection has a non-contiguous element layout, then developers currently will need to convert to or from array/span types at the interop boundary. This section proposes an API that would enable developers to convert directly between a managed and native non-contiguous collection layout as part of marshalling.

A new marshaller kind named GenericCollection could be added that would specify that the collection is noncontiguous in either managed or native representations. Then additional methods should be added to the generic collection model, and some methods would be removed:

- [CustomTypeMarshaller(typeof(Span<>), CustomTypeMarshallerKind.LinearCollection)]
+ [CustomTypeMarshaller(typeof(Span<>), CustomTypeMarshallerKind.GenericCollection)]
public struct GenericContiguousCollectionMarshallerImpl<T, U, V,...>
{
    // these constructors are required if marshalling from managed to native is supported.
    public GenericContiguousCollectionMarshallerImpl(GenericCollection<T, U, V, ...> collection, int nativeSizeOfElements);
    public GenericContiguousCollectionMarshallerImpl(GenericCollection<T, U, V, ...> collection, Span<byte> stackSpace, int nativeSizeOfElements); // optional

    public TNative ToNativeValue();
    public void FromNativeValue(TNative value);

-   public ReadOnlySpan<TCollectionElement> GetManagedValuesSource();
-   public Span<TCollectionElement> GetManagedValuesDestination(int length);
-   public ReadOnlySpan<byte> GetNativeValuesSource(int length);
-   public Span<byte> GetNativeValuesDestination();

+    public ref byte GetOffsetForNativeValueAtIndex(int index);
+    public TCollectionElement GetManagedValueAtIndex(int index);
+    public TCollectionElement SetManagedValueAtIndex(int index);
+    public int Count { get; set; }
}

The GetManagedValueAtIndex method and Count getter are used in the process of marshalling from managed to native. The generated code will iterate through Count elements (retrieved through GetManagedValueAtIndex) and assign their marshalled result to the address represented by GetOffsetForNativeValueAtIndex called with the same index. Then the ToNativeValue method will be called to get the value to pass to native code.

The SetManagedValueAtIndex method and the Count setter are used in the process of marshalling from native to managed. The Count property will be set to the number of elements that the native collection contains, and the FromNativeValue property will be called with the result value from native code. Then the stub will iterate through the native collection Count times, calling GetOffsetForNativeValueAtIndex to get the offset of the native value and calling SetManagedValueAtIndex to set the unmarshalled managed value at that index.

Pros/Cons of Design 2

Pros:

Cons: