Skip to content

Commit 06a0c76

Browse files
Update GetPinnableReference() usage in multi-step custom type marshalling (#61539)
* Update GetPinnableReference() usage in multi-step custom tyep marshalling Change GetPinnableReference() on a marshaller type to be used as a side effect when marshalling in all cases when a fixed() statement is usable. Use the Value property getter to get the value to pass to native in all cases. Update our marshallers and tests to follow this design update. * Apply suggestions from code review Co-authored-by: Aaron Robinson <arobins@microsoft.com> * Don't emit the assingment to the Value property in the Unmarshal stage when using [Out] * Fix the unmarshalling conditions * Fix Utf16StringMarshaller to correctly handle the "null pointer" case now that we're using spans internally for storage in all cases. Co-authored-by: Aaron Robinson <arobins@microsoft.com>
1 parent 87b92fd commit 06a0c76

File tree

14 files changed

+418
-281
lines changed

14 files changed

+418
-281
lines changed

docs/design/libraries/DllImportGenerator/StructMarshalling.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,16 @@ partial struct TNative
5454
}
5555
```
5656

57-
The analyzer will report an error if neither the construtor nor the ToManaged method is defined. When one of those two methods is missing, the direction of marshalling (managed to native/native to managed) that relies on the missing method is considered unsupported for the corresponding managed type. The FreeNative method is only required when there are resources that need to be released.
57+
The analyzer will report an error if neither the constructor nor the `ToManaged` method is defined. When one of those two methods is missing, the direction of marshalling (managed to native/native to managed) that relies on the missing method is considered unsupported for the corresponding managed type. The `FreeNative` method is only required when there are resources that need to be released.
5858

5959

6060
> :question: Does this API surface and shape work for all marshalling scenarios we plan on supporting? It may have issues with the current "layout class" by-value `[Out]` parameter marshalling where the runtime updates a `class` typed object in place. We already recommend against using classes for interop for performance reasons and a struct value passed via `ref` or `out` with the same members would cover this scenario.
6161
6262
If the native type `TNative` also has a public `Value` property, then the value of the `Value` property will be passed to native code instead of the `TNative` value itself. As a result, the type `TNative` will be allowed to be non-blittable and the type of the `Value` property will be required to be blittable. If the `Value` property is settable, then when marshalling in the native-to-managed direction, a default value of `TNative` will have its `Value` property set to the native value. If `Value` does not have a setter, then marshalling from native to managed is not supported.
6363

64-
A `ref` or `ref readonly` typed `Value` property is unsupported. If a ref-return is required, the type author can supply a `GetPinnableReference` method on the native type. If a `GetPinnableReference` method is supplied, then the `Value` property must have a pointer-sized primitive type.
64+
If a `Value` property is provided, the developer may also provide a ref-returning or readonly-ref-returning `GetPinnableReference` method. The `GetPinnableReference` method will be called before the `Value` property getter is called. The ref returned by `GetPinnableReference` will be pinned with a `fixed` statement, but the pinned value will not be used (it acts exclusively as a side-effect).
65+
66+
A `ref` or `ref readonly` typed `Value` property is unsupported. If a ref-return is required, the type author can supply a `GetPinnableReference` method on the native type to pin the desired `ref` to return and then use `System.Runtime.CompilerServices.Unsafe.AsPointer` to get a pointer from the `ref` that will have already been pinned by the time the `Value` getter is called.
6567

6668
```csharp
6769
[NativeMarshalling(typeof(TMarshaler))]
@@ -72,14 +74,14 @@ public struct TManaged
7274

7375
public struct TMarshaler
7476
{
75-
public TNative(TManaged managed) {}
77+
public TMarshaler(TManaged managed) {}
7678
public TManaged ToManaged() {}
7779

7880
public void FreeNative() {}
7981

8082
public ref TNative GetPinnableReference() {}
8183

82-
public TNative Value { get; set; }
84+
public TNative* Value { get; set; }
8385
}
8486

8587
```
@@ -88,7 +90,7 @@ public struct TMarshaler
8890

8991
#### Pinning
9092

91-
Since C# 7.3 added a feature to enable custom pinning logic for user types, we should also add support for custom pinning logic. If the user provides a `GetPinnableReference` method that matches the requirements to be used in a `fixed` statement and the pointed-to type is blittable, then we will support using pinning to marshal the managed value when possible. The analyzer should issue a warning when the pointed-to type would not match the final native type, accounting for the `Value` property on the native type. Since `MarshalUsingAttribute` is applied at usage time instead of at type authoring time, we will not enable the pinning feature since the implementation of `GetPinnableReference` unless the pointed-to return type matches the native type.
93+
Since C# 7.3 added a feature to enable custom pinning logic for user types, we should also add support for custom pinning logic. If the user provides a `GetPinnableReference` method on the managed type that matches the requirements to be used in a `fixed` statement and the pointed-to type is blittable, then we will support using pinning to marshal the managed value when possible. The analyzer should issue a warning when the pointed-to type would not match the final native type, accounting for the `Value` property on the native type. Since `MarshalUsingAttribute` is applied at usage time instead of at type authoring time, we will not enable the pinning feature since the implementation of `GetPinnableReference` is likely designed to match the default marshalling rules provided by the type author, not the rules provided by the marshaller provided by the `MarshalUsingAttribute`.
9294

9395
#### Caller-allocated memory
9496

@@ -100,14 +102,14 @@ partial struct TNative
100102
public TNative(TManaged managed, Span<byte> buffer) {}
101103

102104
public const int BufferSize = /* */;
103-
105+
104106
public const bool RequiresStackBuffer = /* */;
105107
}
106108
```
107109

108110
When these members are present, the source generator will call the two-parameter constructor with a possibly stack-allocated buffer of `BufferSize` bytes when a stack-allocated buffer is usable. If a stack-allocated buffer is a requirement, the `RequiresStackBuffer` field should be set to `true` and the `buffer` will be guaranteed to be allocated on the stack. Setting the `RequiresStackBuffer` field to `false` is the same as omitting the field definition. Since a dynamically allocated buffer is not usable in all scenarios, for example Reverse P/Invoke and struct marshalling, a one-parameter constructor must also be provided for usage in those scenarios. This may also be provided by providing a two-parameter constructor with a default value for the second parameter.
109111

110-
Type authors can pass down the `buffer` pointer to native code by defining a `GetPinnableReference()` method on the native type that returns a reference to the first element of the span. When the `RequiresStackBuffer` field is set to `true`, the type author is free to use APIs that would be dangerous in non-stack-allocated scenarios such as `MemoryMarshal.GetReference()` and `Unsafe.AsPointer()`.
112+
Type authors can pass down the `buffer` pointer to native code by defining a `Value` property that returns a pointer to the first element, generally through code using `MemoryMarshal.GetReference()` and `Unsafe.AsPointer`. If `RequiresStackBuffer` is not provided or set to `false`, the `buffer` span must be pinned to be used safely. The `buffer` span can be pinned by defining a `GetPinnableReference()` method on the native type that returns a reference to the first element of the span.
111113

112114
### Usage
113115

src/libraries/Common/src/System/Runtime/InteropServices/ArrayMarshaller.cs

Lines changed: 6 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,8 @@ public ArrayMarshaller(int sizeOfNativeElement)
3232
}
3333

3434
public ArrayMarshaller(T[]? managed, int sizeOfNativeElement)
35+
:this(managed, Span<byte>.Empty, sizeOfNativeElement)
3536
{
36-
_allocatedMemory = default;
37-
_sizeOfNativeElement = sizeOfNativeElement;
38-
if (managed is null)
39-
{
40-
_managedArray = null;
41-
NativeValueStorage = default;
42-
return;
43-
}
44-
_managedArray = managed;
45-
// Always allocate at least one byte when the array is zero-length.
46-
int spaceToAllocate = Math.Max(managed.Length * _sizeOfNativeElement, 1);
47-
_allocatedMemory = Marshal.AllocCoTaskMem(spaceToAllocate);
48-
NativeValueStorage = new Span<byte>((void*)_allocatedMemory, spaceToAllocate);
4937
}
5038

5139
public ArrayMarshaller(T[]? managed, Span<byte> stackSpace, int sizeOfNativeElement)
@@ -94,8 +82,7 @@ public byte* Value
9482
{
9583
get
9684
{
97-
Debug.Assert(_managedArray is null || _allocatedMemory != IntPtr.Zero);
98-
return (byte*)_allocatedMemory;
85+
return (byte*)Unsafe.AsPointer(ref GetPinnableReference());
9986
}
10087
set
10188
{
@@ -116,10 +103,7 @@ public byte* Value
116103

117104
public void FreeNative()
118105
{
119-
if (_allocatedMemory != IntPtr.Zero)
120-
{
121-
Marshal.FreeCoTaskMem(_allocatedMemory);
122-
}
106+
Marshal.FreeCoTaskMem(_allocatedMemory);
123107
}
124108
}
125109

@@ -141,20 +125,8 @@ public PtrArrayMarshaller(int sizeOfNativeElement)
141125
}
142126

143127
public PtrArrayMarshaller(T*[]? managed, int sizeOfNativeElement)
128+
:this(managed, Span<byte>.Empty, sizeOfNativeElement)
144129
{
145-
_allocatedMemory = default;
146-
_sizeOfNativeElement = sizeOfNativeElement;
147-
if (managed is null)
148-
{
149-
_managedArray = null;
150-
NativeValueStorage = default;
151-
return;
152-
}
153-
_managedArray = managed;
154-
// Always allocate at least one byte when the array is zero-length.
155-
int spaceToAllocate = Math.Max(managed.Length * _sizeOfNativeElement, 1);
156-
_allocatedMemory = Marshal.AllocCoTaskMem(spaceToAllocate);
157-
NativeValueStorage = new Span<byte>((void*)_allocatedMemory, spaceToAllocate);
158130
}
159131

160132
public PtrArrayMarshaller(T*[]? managed, Span<byte> stackSpace, int sizeOfNativeElement)
@@ -203,8 +175,7 @@ public byte* Value
203175
{
204176
get
205177
{
206-
Debug.Assert(_managedArray is null || _allocatedMemory != IntPtr.Zero);
207-
return (byte*)_allocatedMemory;
178+
return (byte*)Unsafe.AsPointer(ref GetPinnableReference());
208179
}
209180
set
210181
{
@@ -226,10 +197,7 @@ public byte* Value
226197

227198
public void FreeNative()
228199
{
229-
if (_allocatedMemory != IntPtr.Zero)
230-
{
231-
Marshal.FreeCoTaskMem(_allocatedMemory);
232-
}
200+
Marshal.FreeCoTaskMem(_allocatedMemory);
233201
}
234202
}
235203
}

0 commit comments

Comments
 (0)