Skip to content

[API Proposal]: Support for Span<T> and ReadOnlySpan<T> in source-generated marshalling #69281

@jkoritzinsky

Description

@jkoritzinsky

Background and motivation

One of the goals of the source-generated marshalling in .NET 7 is to support modern types at interop boundaries. In particular, there has been significant interest in using Span<T> and ReadOnlySpan<T> at interop boundaries. This API proposal includes a number of different Span-based marshallers. This proposal does not include defining any of these marshaller types as the "default" marshallers for span types; however, we can choose to define some of these as defaults if we so desire.

  1. SpanMarshaller<T> and ReadOnlySpanMarshaller<T>. These types marshal the span values in the same style as arrays. However, since the Span types treat an empty span as identical to a null span, these types pass null to native code when an empty span is passed as the managed value.
  2. NeverNullSpanMarshaller<T> and NeverNullReadOnlySpanMarshaller<T>. These types marshal the span values similarly to SpanMarshaller<T> and ReadOnlySpanMarshaller<T>, but when an empty or null span is passed as a value, the marshaller passes a non-null value to native code that can be dereferenced but should not be written to. This allows developers to opt-in to similar behavior as arrays (where we don't pass null when the input is an empty array, but instead pass a reference to where the zeroth element would live).

We also propose updating the source generator to recognize a static GetPinnableReference method of the same shape as an extension GetPinnableReference method on the marshaller type that takes the managed type. The marshallers will implement this pattern to provide a faster path for by-value P/Invoke scenarios without requiring them to be specified as the default marshallers for the types.

It seems that we want to make the ReadOnlySpanMarshaller and SpanMarshaller types the default marshallers for their respective types. If we want to do so, then we'll add [NativeMarshalling] attributes to the managed types pointing at these marshallers.

API Proposal

namespace System.Runtime.InteropServices.Marshalling
{
    [CustomTypeMarshaller(typeof(ReadOnlySpan<>), CustomTypeMarshallerKind.LinearCollection, Direction = CustomTypeMarshallerDirection.In, Features = CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.CallerAllocatedBuffer | CustomTypeMarshallerFeatures.TwoStageMarshalling, BufferSize = 0x200)]
    public unsafe ref struct ReadOnlySpanMarshaller<T>
    {
        public ReadOnlySpanMarshaller(int sizeOfNativeElement);

        public ReadOnlySpanMarshaller(ReadOnlySpan<T> managed, int sizeOfNativeElement);

        public ReadOnlySpanMarshaller(ReadOnlySpan<T> managed, Span<byte> stackSpace, int sizeOfNativeElement);

        public ReadOnlySpan<T> GetManagedValuesSource();
        public Span<byte> GetNativeValuesDestination();
        public ref byte GetPinnableReference();
        public byte* ToNativeValue();
        public void FreeNative();
        public static ref T GetPinnableReference(ReadOnlySpan<T> managed);
    }

    [CustomTypeMarshaller(typeof(Span<>), CustomTypeMarshallerKind.LinearCollection, Features = CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.CallerAllocatedBuffer | CustomTypeMarshallerFeatures.TwoStageMarshalling, BufferSize = 0x200)]
    public unsafe ref struct SpanMarshaller<T>
    {
        public SpanMarshaller(int sizeOfNativeElement);

        public SpanMarshaller(Span<T> managed, int sizeOfNativeElement);

        public SpanMarshaller(Span<T> managed, Span<byte> stackSpace, int sizeOfNativeElement);


        public ReadOnlySpan<T> GetManagedValuesSource();
        public Span<T> GetManagedValuesDestination(int length);
        public Span<byte> GetNativeValuesDestination();
        public ReadOnlySpan<byte> GetNativeValuesSource(int length);
        public ref byte GetPinnableReference();
        public byte* ToNativeValue();
        public void FromNativeValue(byte* value);
        public static ref T GetPinnableReference(Span<T> managed);

        public Span<T> ToManaged();

        public void FreeNative();
    }

    [CustomTypeMarshaller(typeof(Span<>), CustomTypeMarshallerKind.LinearCollection, Features = CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.CallerAllocatedBuffer | CustomTypeMarshallerFeatures.TwoStageMarshalling, BufferSize = 0x200)]
    public unsafe ref struct NeverNullSpanMarshaller<T>
    {
        public NeverNullSpanMarshaller(int sizeOfNativeElement);

        public NeverNullSpanMarshaller(Span<T> managed, int sizeOfNativeElement);

        public NeverNullSpanMarshaller(Span<T> managed, Span<byte> stackSpace, int sizeOfNativeElement);

        public ReadOnlySpan<T> GetManagedValuesSource();
        public Span<T> GetManagedValuesDestination(int length);
        public Span<byte> GetNativeValuesDestination();
        public ReadOnlySpan<byte> GetNativeValuesSource(int length);
        public ref byte GetPinnableReference();
        public byte* ToNativeValue();
        public void FromNativeValue(byte* value);
        public static ref T GetPinnableReference(Span<T> managed);

        public Span<T> ToManaged();

        public void FreeNative();
    }

    [CustomTypeMarshaller(typeof(ReadOnlySpan<>), CustomTypeMarshallerKind.LinearCollection, Direction = CustomTypeMarshallerDirection.In, Features = CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.CallerAllocatedBuffer | CustomTypeMarshallerFeatures.TwoStageMarshalling, BufferSize = 0x200)]
    public unsafe ref struct NeverNullReadOnlySpanMarshaller<T>
    {
        public NeverNullReadOnlySpanMarshaller(int sizeOfNativeElement);

        public NeverNullReadOnlySpanMarshaller(ReadOnlySpan<T> managed, int sizeOfNativeElement);

        public NeverNullReadOnlySpanMarshaller(ReadOnlySpan<T> managed, Span<byte> stackSpace, int sizeOfNativeElement);

        public ReadOnlySpan<T> GetManagedValuesSource();
        public Span<byte> GetNativeValuesDestination();
        public ref byte GetPinnableReference();
        public byte* ToNativeValue();
        public static ref T GetPinnableReference(ReadOnlySpan<T> managed);

        public void FreeNative();
    }
}

API Usage

[LibraryImport("MyNativeLib")]
static partial int SumValues([MarshalUsing(typeof(SpanMarshaller<int>))] Span<int> values, int numValues);

Alternative Designs

No response

Risks

As these are new marshallers, we can choose to use different allocators for the native memory if we so choose. However, if we choose an allocator different from the array marshallers, then we break compatibility between array marshallers and span marshallers in the by-ref case (ref Span<T> vs ref T[]). We could add an additional INativeAllocator interface and implementations for our various allocators, and add a second generic parameter to the span marshallers (and the array marshallers if we desire as they haven't shipped yet in an official release) to allow developers to pass in the allocator to use.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions