[API Proposal]: System.Runtime.InteropServices.Marshalling.MarshalContentsAttribute to describe how interop source generators should marshal contents of collections
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background and motivation
Traditionally, .NET interop has used the `System.Runtime.InteropServices.InAttribute` and the `System.Runtime.InteropServices.OutAttribute` to specify how to pass contents of arrays in P/Invoke scenarios. However, these attributes have inconsistent default behaviors depending on the blittability of the array element types. The "default" experience is `[In]` (pass elements to native, but don't propagate back changes), but if the array element is blittable, the default behavior is `[In, Out]` with no way to override it (specifying `[Out]` does nothing other than specify intent).
This API proposal proposes introducing a mechanism for users to not only specify intent for collection element marshalling, but also influence codegen even for scenarios where pinning is supported (allowing users to opt in to using the non-pinning path if they want to ensure that the managed buffer is not changed).
The interop source generators will provide recommendations (suggestion level) for all collection arguments to specify this attribute to provide explicit intent. To avoid breaking compatibility, we will not make this attribute required, only recommended.
### API Proposal
```csharp
namespace System.Runtime.InteropServices.Marshalling;
[AttributeUsage(AttributeTargets.Parameter)]
class MarshalContentsAttribute(MarshalContentsKind kind);
enum MarshalContentsKind
{
PassOrPin = 0, // Pass the managed contents of the collection to native code, using pinning if possible (which can allow unmanaged code to edit values in the buffer). Equivalent to [In]. Default.
PassOnly = 1, // Pass the managed contents of the collection to native code. Do not use any pinning optimization to avoid allowing unmanaged code to edit managed values
PassAndFill = 2, // Pass the managed contents of the collection to native code. On return, marshal the contents of the collection back to managed code. May use pinning optimizations if available. Equivalent to [In, Out]
FillOnly = 3, // Don't marshal the contents of the collection to native code. On return, marshal the contents of the collection back to managed code.
FillOrPin = 4, // Don't marshal the contents of the collection to native code. On return, marshal the contents of the collection back to managed code. Use pinning to pass the buffer if possible, which can result in exposing the existing contents to native code. Equivalent to [Out]
}
```
### API Usage
```csharp
[LibraryImport(ModuleName)]
public static partial void PassReadOnlyBuffer([MarshalUsing(CountElementName = nameof(length)), MarshalContents(MarshalContentsKind.PassOnly]int[] buffer, int length);
[LibraryImport(ModuleName)]
public static partial void FillUninitializedBuffer([MarshalUsing(CountElementName = nameof(length)), MarshalContents(MarshalContentsKind.FillOnly] int[] buffer, int length);
[LibraryImport(ModuleName)]
public static partial void DoubleElementsInBuffer([MarshalUsing(CountElementName = nameof(length)), MarshalContents(MarshalContentsKind.PassAndFill] int[] buffer, int length);
```
### Alternative Designs
Alternatively, we could keep with the existing `In` and `Out` attributes and expand them to user-defined collection marshalling with the existing ambiguity and ability to only provide intent, not behavior change, in some scenarios.
### Risks
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.