Skip to content

Commit 4f43e82

Browse files
authored
[DllImportGenerator] Initialize by-value out array to default (#61431)
1 parent d15f391 commit 4f43e82

File tree

4 files changed

+60
-6
lines changed

4 files changed

+60
-6
lines changed

docs/design/libraries/DllImportGenerator/Compatibility.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ Only single-dimensional arrays are supported for source generated marshalling.
7171

7272
In the source-generated marshalling, arrays will be allocated on the stack (instead of through [`AllocCoTaskMem`](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshal.alloccotaskmem)) if they are passed by value or by read-only reference if they contain at most 256 bytes of data. The built-in system does not support this optimization for arrays.
7373

74+
In the built-in system, marshalling a `char` array by value with `CharSet.Unicode` would default to also marshalling data out. In the source-generated marshalling, the `char` array must be marked with the `[Out]` attribute for data to be marshalled out by value.
75+
7476
### `in` keyword
7577

7678
For some types - blittable or Unicode `char` - passed by read-only reference via the [`in` keyword](https://docs.microsoft.com/dotnet/csharp/language-reference/keywords/in-parameter-modifier), the built-in system simply pins the parameter. The generated marshalling code does the same, such that there is no behavioural difference. A consequence of this behaviour is that any modifications made by the invoked function will be reflected in the caller. It is left to the user to avoid the situation in which `in` is used for a parameter that will actually be modified by the invoked function.

src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/Marshalling/ICustomNativeTypeMarshallingStrategy.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,18 @@ public IEnumerable<StatementSyntax> GenerateMarshalStatements(TypePositionInfo i
977977
if (!info.IsByRef && info.ByValueContentsMarshalKind == ByValueContentsMarshalKind.Out)
978978
{
979979
// If the parameter is marshalled by-value [Out], then we don't marshal the contents of the collection.
980+
// We do clear the span, so that if the invoke target doesn't fill it, we aren't left with undefined content.
981+
// <nativeIdentifier>.NativeValueStorage.Clear();
982+
string nativeIdentifier = context.GetIdentifiers(info).native;
983+
yield return ExpressionStatement(
984+
InvocationExpression(
985+
MemberAccessExpression(
986+
SyntaxKind.SimpleMemberAccessExpression,
987+
MemberAccessExpression(
988+
SyntaxKind.SimpleMemberAccessExpression,
989+
IdentifierName(nativeIdentifier),
990+
IdentifierName(ManualTypeMarshallingHelper.NativeValueStoragePropertyName)),
991+
IdentifierName("Clear"))));
980992
yield break;
981993
}
982994

src/libraries/System.Runtime.InteropServices/tests/DllImportGenerator.Tests/ArrayTests.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public partial class Arrays
3838
[GeneratedDllImport(NativeExportsNE_Binary, EntryPoint = "sum_char_array", CharSet = CharSet.Unicode)]
3939
public static partial int SumChars(char[] chars, int numElements);
4040

41+
[GeneratedDllImport(NativeExportsNE_Binary, EntryPoint = "fill_char_array", CharSet = CharSet.Unicode)]
42+
public static partial void FillChars([Out] char[] chars, int length, ushort start);
43+
4144
[GeneratedDllImport(NativeExportsNE_Binary, EntryPoint = "reverse_char_array", CharSet = CharSet.Unicode)]
4245
public static partial void ReverseChars([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] ref char[] chars, int numElements);
4346

@@ -253,12 +256,34 @@ public void DynamicSizedArrayWithConstantComponent()
253256
[Fact]
254257
public void ArrayByValueOutParameter()
255258
{
256-
var testArray = new IntStructWrapper[10];
257-
int start = 5;
258-
259-
NativeExportsNE.Arrays.FillRangeArray(testArray, testArray.Length, start);
260-
261-
Assert.Equal(Enumerable.Range(start, 10), testArray.Select(wrapper => wrapper.Value));
259+
{
260+
var testArray = new IntStructWrapper[10];
261+
int start = 5;
262+
263+
NativeExportsNE.Arrays.FillRangeArray(testArray, testArray.Length, start);
264+
Assert.Equal(Enumerable.Range(start, testArray.Length), testArray.Select(wrapper => wrapper.Value));
265+
266+
// Any items not populated by the invoke target should be initialized to default
267+
testArray = new IntStructWrapper[10];
268+
int lengthToFill = testArray.Length / 2;
269+
NativeExportsNE.Arrays.FillRangeArray(testArray, lengthToFill, start);
270+
Assert.Equal(Enumerable.Range(start, lengthToFill), testArray[..lengthToFill].Select(wrapper => wrapper.Value));
271+
Assert.All(testArray[lengthToFill..], wrapper => Assert.Equal(0, wrapper.Value));
272+
}
273+
{
274+
var testArray = new char[10];
275+
ushort start = 65;
276+
277+
NativeExportsNE.Arrays.FillChars(testArray, testArray.Length, start);
278+
Assert.Equal(Enumerable.Range(start, testArray.Length), testArray.Select(c => (int)c));
279+
280+
// Any items not populated by the invoke target should be initialized to default
281+
testArray = new char[10];
282+
int lengthToFill = testArray.Length / 2;
283+
NativeExportsNE.Arrays.FillChars(testArray, lengthToFill, start);
284+
Assert.Equal(Enumerable.Range(start, lengthToFill), testArray[..lengthToFill].Select(c => (int)c));
285+
Assert.All(testArray[lengthToFill..], c => Assert.Equal(0, c));
286+
}
262287
}
263288

264289
[Fact]

src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/Arrays.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ public static int SumChars(ushort* values, int numValues)
106106
return sum;
107107
}
108108

109+
[UnmanagedCallersOnly(EntryPoint = "fill_char_array")]
110+
public static void FillChars(ushort* values, int length, ushort start)
111+
{
112+
if (values == null)
113+
{
114+
return;
115+
}
116+
117+
ushort val = start;
118+
for (int i = 0; i < length; i++)
119+
{
120+
values[i] = val++;
121+
}
122+
}
123+
109124
[UnmanagedCallersOnly(EntryPoint = "reverse_char_array")]
110125
public static void ReverseChars(ushort** values, int numValues)
111126
{

0 commit comments

Comments
 (0)