-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Comments
Tagging subscribers to this area: @dotnet/interop-contrib Issue DetailsBackground and motivationOne 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
API Proposalnamespace 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 DesignsNo response RisksAs 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 (
|
For the 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?). |
Are there examples of uses for each of these marshallers in our own dotnet/runtime libraries? |
Separately, we should introduce marshallers for marshalling |
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. |
The interop layers in the crypto stack could heavily use the I don't know of any use cases for I'm concerned about using I like the idea of adding a " |
Is the 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?
It suggests that we may not need it. |
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 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. |
If we set them as the default marshallers for
The marshaller that achieves equivalent behavior to that would be the 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 |
Thanks. In that case I think we should make |
Would this marshaller make the
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 runtime/src/coreclr/vm/ilmarshalers.cpp Line 3959 in c96e470
|
I am not sure whether it is common enough to create a public marshaller for this. If we are going to introduce this marshaller:
|
Today the marshallers are using
It will get the inline
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 |
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 Would it make sense to use some kind of a valid pointer (e.g. address of a local) as the sentinel value? |
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. |
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. |
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 |
@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? |
Looks good to me. I assume that generated code for
Is that right? |
Yes, that's the plan. |
A word of caution: we should be extremely careful before we start encouraging people to use 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 |
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();
}
} |
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>
andReadOnlySpan<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.SpanMarshaller<T>
andReadOnlySpanMarshaller<T>
. These types marshal the span values in the same style as arrays. However, since theSpan
types treat an empty span as identical to a null span, these types passnull
to native code when an empty span is passed as the managed value.NeverNullSpanMarshaller<T>
andNeverNullReadOnlySpanMarshaller<T>
. These types marshal the span values similarly toSpanMarshaller<T>
andReadOnlySpanMarshaller<T>
, but when an empty ornull
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 passnull
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 extensionGetPinnableReference
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
andSpanMarshaller
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
API Usage
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>
vsref T[]
). We could add an additionalINativeAllocator
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.The text was updated successfully, but these errors were encountered: