Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jkoritzinsky opened this issue May 12, 2022 · 23 comments · Fixed by #71989
Closed
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.InteropServices source-generator Indicates an issue with a source generator feature
Milestone

Comments

@jkoritzinsky
Copy link
Member

jkoritzinsky commented May 12, 2022

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.

@jkoritzinsky jkoritzinsky added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.InteropServices labels May 12, 2022
@ghost ghost added the untriaged New issue has not been triaged by the area owner label May 12, 2022
@ghost
Copy link

ghost commented May 12, 2022

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

Issue Details

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 sentinel value to native code. This allows developers to opt-in to similar behavior as arrays (where we don't pass null when the input is an empty array).
  3. DirectSpanMarshaller<T>. This marshaller only supports Spans of unmanaged element types. When passing a value from managed to unmanaged, this marshaller behaves the same as SpanMarshaller<T>. When passing a value from unmanaged to managed, this marshaller does not copy memory from native code to managed storage; it creates a span directly over the unmanaged memory. It is then the responsibility of the user to free the memory.

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();
    }

    [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 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 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 void FreeNative();
    }

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

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

        public DirectSpanMarshaller(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 T GetPinnableReference();

        public T* ToNativeValue();

        public void FromNativeValue(T* value);

        public Span<T> ToManaged();

        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.

Author: jkoritzinsky
Assignees: -
Labels:

api-suggestion, area-System.Runtime.InteropServices

Milestone: -

@jkoritzinsky jkoritzinsky removed the untriaged New issue has not been triaged by the area owner label May 12, 2022
@jkoritzinsky jkoritzinsky added this to the 7.0.0 milestone May 12, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 12, 2022
@jkoritzinsky
Copy link
Member Author

For the INativeAllocator concept, here's a sketched-out API proposal if we decide to go that route:

namespace System.Runtime.InteropServices.Marshalling;

public unsafe interface INativeAllocator
{
    static abstract void* Alloc(nuint numBytes);
    static abstract void* AlignedAlloc(nuint numBytes, nuint alignment);
    static abstract void Free(void* ptr);
}

public sealed class CoTaskMemAllocator : INativeAllocator
{
    public static void* Alloc(nuint numBytes);
    public static void* AlignedAlloc(nuint numBytes, nuint alignment);
    public static void Free(void* ptr);
}
public sealed class NativeMemoryAllocator : INativeAllocator
{
    public static void* Alloc(nuint numBytes);
    public static void* AlignedAlloc(nuint numBytes, nuint alignment);
    public static void Free(void* ptr);
}
public sealed class HGlobalAllocator : INativeAllocator
{
    public static void* Alloc(nuint numBytes);
    public static void* AlignedAlloc(nuint numBytes, nuint alignment);
    public static void Free(void* ptr);
}

We could also go an instance route with requiring a default constructor if we think there are any use cases for that (memory pooling?).

@jkotas
Copy link
Member

jkotas commented May 12, 2022

Are there examples of uses for each of these marshallers in our own dotnet/runtime libraries?

@jkotas
Copy link
Member

jkotas commented May 12, 2022

Separately, we should introduce marshallers for marshalling ReadOnlySpan<char> as string. There are multiple examples where such marshaller can be used in our own dotnet/runtime libraries. For example, look for ValueUtf8Converter that is used for marshalling ReadOnlySpan<char> as string in some cases today.

@jkotas
Copy link
Member

jkotas commented May 12, 2022

DirectSpanMarshaller. This marshaller only supports Spans of unmanaged element types. When passing a value from managed to unmanaged, this marshaller behaves the same as SpanMarshaller.

Should this marshaller be the default marshaller for Spans and be special-cased to just pin the array in the case of managed to unmanaged marshalling? Same as how arrays work.

@jkoritzinsky
Copy link
Member Author

The interop layers in the crypto stack could heavily use the SpanMarshaller and ReadOnlySpanMarshaller marshallers for some methods where the length is guaranteed to be non-zero (for example Interop.AndroidCrypto.RsaVerificationPrimitive) and the NeverNull variants where zero-length input is valid (ex. ``Interop.AndroidCrypto.RsaPublicEncrypt). They currently call MemoryMarshal.GetReference` manually.

I don't know of any use cases for DirectSpanMarshaller in dotnet/runtime.

I'm concerned about using DirectSpanMarshaller as the default as the behavior around ownership is very different (ownership of the unmanaged memory passes back to the managed user in ref, out, and return scenarios) and the mechanism to get the pointer to release is very obtuse (Unsafe.AsPointer(ref MemoryMarshal.GetReference(span))).

I like the idea of adding a "ReadOnlySpan<char> as a string" marshaller. That seems very useful! (Maybe we should do one for ReadOnlySpan<byte> as a UTF-8 string as well since C# is adding support for UTF-8 string literals?)

@jkotas
Copy link
Member

jkotas commented May 13, 2022

The interop layers in the crypto stack could heavily use the SpanMarshaller and ReadOnlySpanMarshaller marshallers for some methods where the length is guaranteed to be non-zero (for example Interop.AndroidCrypto.RsaVerificationPrimitive)

Is the ReadOnlySpanMarshaller / SpanMarshaller going to be treated as intrinsic in source generator and compile to just fixed statement without actually instantiating the ReadOnlySpanMarshaller / SpanMarshaller?

Do we have any examples of Span marshalling in dotnet/runtime libraries that is not just a simple pinning and that copies the data over or transfers ownership?

I don't know of any use cases for DirectSpanMarshaller in dotnet/runtime.

It suggests that we may not need it.

@stephentoub
Copy link
Member

stephentoub commented May 13, 2022

I'm having trouble deciphering the descriptions. Given an example like this:

                fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
                {
                    if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                      flags,
                                                      pSource, sourceLength /* in chars */,
                                                      pSortKey, sortKeyLength,
                                                      null, null, _sortHandle) != sortKeyLength)
                    {
                        throw new ArgumentException(SR.Arg_ExternalException);
                    }
                }

and I want to be able to just pass span to LCMapStringEx instead of doing the fixed myself and passing pSortKey, which of these marshalers achieves that with no additional ceremony?

Similarly for:

        internal static int BCryptEncrypt(SafeKeyHandle hKey, ReadOnlySpan<byte> input, byte[]? iv, Span<byte> output)
        {
            unsafe
            {
                fixed (byte* pbInput = input)
                fixed (byte* pbOutput = output)
                {
                    int cbResult;
                    NTSTATUS ntStatus = BCryptEncrypt(hKey, pbInput, input.Length, IntPtr.Zero, iv, iv == null ? 0 : iv.Length, pbOutput, output.Length, out cbResult, 0);

                    if (ntStatus != NTSTATUS.STATUS_SUCCESS)
                    {
                        throw CreateCryptographicException(ntStatus);
                    }

                    return cbResult;
                }
            }
        }

just wanting to call BCryptEncrypt(hKey, input, input.Length, ..., output, output.Length, ...) and have the marshaling just pin the input and output and pass the pointers, which of these marshalers achieves just that?

These are just random examples, but I'd wager that the vast majority of places we need to marshal spans in dotnet/runtime are of this ilk.

@AaronRobinsonMSFT AaronRobinsonMSFT added api-suggestion Early API idea and discussion, it is NOT ready for implementation and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 13, 2022
@jkoritzinsky
Copy link
Member Author

jkoritzinsky commented May 13, 2022

The interop layers in the crypto stack could heavily use the SpanMarshaller and ReadOnlySpanMarshaller marshallers for some methods where the length is guaranteed to be non-zero (for example Interop.AndroidCrypto.RsaVerificationPrimitive)

Is the ReadOnlySpanMarshaller / SpanMarshaller going to be treated as intrinsic in source generator and compile to just fixed statement without actually instantiating the ReadOnlySpanMarshaller / SpanMarshaller?

If we set them as the default marshallers for ReadOnlySpan and Span respectively, then they'll just use fixed statements (we use the GetPinnableReference method on a "managed" type when possible when we are using the default marshaller for a type).

Do we have any examples of Span marshalling in dotnet/runtime libraries that is not just a simple pinning and that copies the data over or transfers ownership?

I don't know of any use cases for DirectSpanMarshaller in dotnet/runtime.

It suggests that we may not need it.

DirectSpanMarshaller is meant for a zero-overhead no-allocation model and also as an example for "something different". It may be useful in some scenarios, but as usage is not drop-in like the other marshallers, it's more difficult to find places where we might use it. It looks like we could use it with Interop.Sys.GetEnviron if we wanted to. If we don't feel it needs to be public, we don't need to expose it (we can keep it as part of our test bed).

I'm having trouble deciphering the descriptions. Given an example like this:

                fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
                {
                    if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
                                                      flags,
                                                      pSource, sourceLength /* in chars */,
                                                      pSortKey, sortKeyLength,
                                                      null, null, _sortHandle) != sortKeyLength)
                    {
                        throw new ArgumentException(SR.Arg_ExternalException);
                    }
                }

and I want to be able to just pass span to LCMapStringEx instead of doing the fixed myself and passing pSortKey, which of these marshalers achieves that with no additional ceremony?

Similarly for:

        internal static int BCryptEncrypt(SafeKeyHandle hKey, ReadOnlySpan<byte> input, byte[]? iv, Span<byte> output)
        {
            unsafe
            {
                fixed (byte* pbInput = input)
                fixed (byte* pbOutput = output)
                {
                    int cbResult;
                    NTSTATUS ntStatus = BCryptEncrypt(hKey, pbInput, input.Length, IntPtr.Zero, iv, iv == null ? 0 : iv.Length, pbOutput, output.Length, out cbResult, 0);

                    if (ntStatus != NTSTATUS.STATUS_SUCCESS)
                    {
                        throw CreateCryptographicException(ntStatus);
                    }

                    return cbResult;
                }
            }
        }

just wanting to call BCryptEncrypt(hKey, input, input.Length, ..., output, output.Length, ...) and have the marshaling just pin the input and output and pass the pointers, which of these marshalers achieves just that?

These are just random examples, but I'd wager that the vast majority of places we need to marshal spans in dotnet/runtime are of this ilk.

The marshaller that achieves equivalent behavior to that would be the SpanMarshaller<T> and the ReadOnlySpanMarshaller<T>, as both of these marshallers return a null ref when the span is empty, just like GetPinnableReference() does on Span<T> and ReadOnlySpan<T>.

If we make these marshallers the default marshallers for the types, then we would generate the exact same code as your second example (the first example technically is different and would require opting in to using NeverNullSpanMarshaller<T> since MemoryMarshal.GetReference(span) can return a different ref that span.GetPinnableReference() for a span over an empty array).

@stephentoub
Copy link
Member

Thanks. In that case I think we should make {ReadOnly}SpanMarshaller<T> the default and emit as efficient code as we can for those semantics when not overriden.

@jkotas
Copy link
Member

jkotas commented May 13, 2022

It looks like we could use it with Interop.Sys.GetEnviron if we wanted to. If we don't feel it needs to be public, we don't need to expose it

Would this marshaller make the Interop.Sys.GetEnviron look better? I do not see how it would help. Until we find compelling examples of where it can be used, I would vote to not expose it.

NeverNullSpanMarshaller

This marshaller should be only useful to work around a broken argument validation (an API incorrectly rejects null for empty buffers). I do not think we need it. I would leave it up to the calling code to work around it for the few corner cases that run into this problem.

@jkoritzinsky
Copy link
Member Author

NeverNullSpanMarshaller

This marshaller should be only useful to work around a broken argument validation (an API incorrectly rejects null for empty buffers). I do not think we need it. I would leave it up to the calling code to work around it for the few corner cases that run into this problem.

This marshaller emulates the default behavior we have for array marshalling because many native APIs do not support passing null instead of zero-length. We've hit this many times, with a few places you've pointed out here: dotnet/runtimelab#1051 (comment). When I initially tried to change the behavior in coreclr, it broke too many places to be worth trying to manually fix (see the compat comment I added

// COMPAT: We cannot generate the same code that the C# compiler generates for
). I feel that this is common enough in native APIs that we should have a way to support it without requiring developers to drop back to manually marshalling their spans or writing their own analyzer.

@jkotas
Copy link
Member

jkotas commented May 13, 2022

I feel that this is common enough in native APIs that we should have a way to support it without requiring developers to drop back to manually marshalling their spans or writing their own analyzer.

I am not sure whether it is common enough to create a public marshaller for this. If we are going to introduce this marshaller:

  • What are we going to use as the place-holder non-null value? (void*)1 that we are using as workaround internally in some places does not always work to work around this problem.
  • Is the [In] NeverNullMarshaller going to be treated as intrinsic that just expands to inline fixed or is it going to only have a slow path that has marshaller instance in the local?

@jkoritzinsky
Copy link
Member Author

I feel that this is common enough in native APIs that we should have a way to support it without requiring developers to drop back to manually marshalling their spans or writing their own analyzer.

I am not sure whether it is common enough to create a public marshaller for this. If we are going to introduce this marshaller:

  • What are we going to use as the place-holder non-null value? (void*)1 that we are using as workaround internally in some places does not always work to work around this problem.

Today the marshallers are using 0xa5a5a5a5 as the sentinel value.

  • Is the [In] NeverNullMarshaller going to be treated as intrinsic that just expands to inline fixed or is it going to only have a slow path that has marshaller instance in the local?

It will get the inline fixed for [In] for 2 reasons.

  1. Only the default marshallers get the inline fixed of the managed object.
  2. The behavior differs between the inline fixed and the marshaller with the zero-length situation.

We could add intrinsic support in the generator for this scenario if we feel it is necessary for performance, but at that point we should be considering if we need to extend the marshaller model to provide a more performant option instead of hard-coding the generator (maybe allow the marshaller type to provide a GetPinnableReference(TManaged) static method that the generator can use).

@jkotas
Copy link
Member

jkotas commented May 13, 2022

Today the marshallers are using 0xa5a5a5a5 as the sentinel value.

If somebody sees this as a pointer value under debugger, they are likely going to think that it is a corrupted pointer, likely caused by use-after-free bug. We are going to get support questions on why .NET runtime passes 0xa5a5a5a5 as pointer value to PInvokes.

Would it make sense to use some kind of a valid pointer (e.g. address of a local) as the sentinel value?

@jkoritzinsky
Copy link
Member Author

We could use a valid pointer; however, I do like the enforcement that people cannot read or write from the address we pass when we know there is no data. It helps ensure that any API that tries to read the data automatically fails. If we want to do this with a different value instead of 0xa5a5a5a5 (maybe with a custom static address that we set the permissions on), we could do that.

If we feel that this protection isn't necessary (and we're fine with code that reads or writes bogus data not failing immediately), we can change this to point to a valid address.

@jkotas
Copy link
Member

jkotas commented May 13, 2022

Yes, this gets ugly. The null pointer is the cleanest way to guarantee that you cannot read or write to it. It is why my first instinct would be to omit the workaround for the broken error validation from the platform public surface.

@jkoritzinsky
Copy link
Member Author

jkoritzinsky commented May 23, 2022

I've updated the proposal. I've removed the DirectSpanMarshaller for now (we'll keep it in our test tree as it's still good test coverage for the marshaller shapes). I've added a new pinning pattern so we can avoid doing as much work for the NeverNullSpanMarshaller types and the regular marshallers without making them the defaults, and I've added a comment about which marshallers we're looking at making the default. I've also changed the statement around what our "never null" value will be.

@jkoritzinsky
Copy link
Member Author

@jkotas any more feedback on the API proposal for the span-based marshallers after my most recent updates, or can I mark this as ready for review?

@jkotas
Copy link
Member

jkotas commented May 27, 2022

Looks good to me.

I assume that generated code for static partial int SumValues([MarshalUsing(typeof(SpanMarshaller<int>))] Span<int> values, int numValues); is going to look like this:

fixed (int* values_native = SpanMarshaller<int>.GetPinnableReference(values))
{
    PInvoke(values_native, numValues);
}

Is that right?

@jkoritzinsky
Copy link
Member Author

Yes, that's the plan.

@jkoritzinsky jkoritzinsky added api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels May 27, 2022
@GrabYourPitchforks
Copy link
Member

A word of caution: we should be extremely careful before we start encouraging people to use ROS<char>/ROS<byte> for LPCWSTR/LPCUTF8 parameters. The callees generally expect these buffers to be null-terminated unless the length is explicitly passed as a second parameter, and spans are not guaranteed null-terminated like string is (and the Utf8String prototype was).

In the samples provided earlier in this issue, numValues was passed as an explicit parameter. But there are tons of places where people pass simple string args (with no explicit length) across p/invoke boundaries, and devs who change those call sites to pass ROS<char> instead of string will almost certainly end up creating bugs within their apps.

@bartonjs
Copy link
Member

bartonjs commented May 31, 2022

Video

  • We removed the NeverNull variants from the approved shape. The name we were leaning toward was NullAsEmpty instead of NeverNull, but we felt that if the default marshaller matches MemoryMarshal.GetReference instead of Span.GetPinnableReference that we won't need it.
  • I added some readonly modifiers to where the refs were in the ReadOnlySpan marshaller. They might be wrong. Make them be right 😄.
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 readonly byte GetPinnableReference();
        public byte* ToNativeValue();
        public void FreeNative();
        public static ref readonly 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();
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 31, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT added the source-generator Indicates an issue with a source generator feature label Jun 17, 2022
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 12, 2022
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 12, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 12, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Runtime.InteropServices source-generator Indicates an issue with a source generator feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants