Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -904,17 +904,34 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverload(
signatureChanged = true;
parameters[paramIndex] = parameters[paramIndex]
.WithType(NullableType(elementType).WithTrailingTrivia(TriviaList(Space)));
leadingStatements.Add(
LocalDeclarationStatement(VariableDeclaration(
elementType,
[
VariableDeclarator(localName.Identifier, EqualsValueClause(
BinaryExpression(SyntaxKind.CoalesceExpression, origName, DefaultExpression(elementType))))
])));
arguments[paramIndex] = Argument(ConditionalExpression(
MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, origName, IdentifierName("HasValue")),
PrefixUnaryExpression(SyntaxKind.AddressOfExpression, localName),
LiteralExpression(SyntaxKind.NullLiteralExpression)));
if (externParam.Type is ArrayTypeSyntax)
{
// The extern method exposes this [In, Optional] managed (non-blittable) struct parameter as an
// array because a pointer to a managed type is illegal and an `in` modifier cannot represent a
// null pointer (passing null would make the marshaler dereference a null reference). Pass a
// single-element array when a value is provided, or null to omit it. A null array marshals to a
// null pointer. See https://github.com/microsoft/CsWin32/issues/1739.
arguments[paramIndex] = Argument(ConditionalExpression(
MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, origName, IdentifierName(nameof(Nullable<int>.HasValue))),
ImplicitArrayCreationExpression(InitializerExpression(
SyntaxKind.ArrayInitializerExpression,
[MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, origName, IdentifierName(nameof(Nullable<int>.Value)))])),
LiteralExpression(SyntaxKind.NullLiteralExpression)));
}
else
{
leadingStatements.Add(
LocalDeclarationStatement(VariableDeclaration(
elementType,
[
VariableDeclarator(localName.Identifier, EqualsValueClause(
BinaryExpression(SyntaxKind.CoalesceExpression, origName, DefaultExpression(elementType))))
])));
arguments[paramIndex] = Argument(ConditionalExpression(
MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, origName, IdentifierName("HasValue")),
PrefixUnaryExpression(SyntaxKind.AddressOfExpression, localName),
LiteralExpression(SyntaxKind.NullLiteralExpression)));
}
}
else if (isIn && isOut)
{
Expand Down
17 changes: 17 additions & 0 deletions src/Microsoft.Windows.CsWin32/PointerTypeHandleInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ elementTypeDetails.MarshalUsingType is string ||
bool xIn = (parameterAttributes & ParameterAttributes.In) == ParameterAttributes.In;
bool xOut = (parameterAttributes & ParameterAttributes.Out) == ParameterAttributes.Out;

// An optional [In] (but not [Out]) parameter that points to a managed (non-blittable) struct cannot be
// exposed as a pointer (pointers to managed types are illegal) and an `in` modifier cannot represent a
// null pointer: passing null makes the marshaler dereference a null reference (NullReferenceException).
// Represent it as an array instead (as we already do for such struct fields below): a null array marshals
// to a null pointer and a single-element array marshals to a pointer to the struct. A friendly overload
// exposes this as a nullable value. See https://github.com/microsoft/CsWin32/issues/1739.
if (xIn && !xOut && xOptional && inputs.AllowMarshaling && nativeArrayInfo is null
&& forElement == Generator.GeneratingElement.ExternMethod
&& inputs.Generator?.IsManagedType(this.ElementType) is true
&& this.ElementType.IsValueType(inputs) is true)
{
return new TypeSyntaxAndMarshaling(
ArrayType(elementTypeDetails.Type, [ArrayRankSpecifier()]),
elementTypeDetails.MarshalAsAttribute is object ? new MarshalAsAttribute(UnmanagedType.LPArray) { ArraySubType = elementTypeDetails.MarshalAsAttribute.Value } : null,
elementTypeDetails.NativeArrayInfo);
}

// A pointer to a marshaled object is not allowed.
if (inputs.AllowMarshaling && customAttributes.HasValue && nativeArrayInfo is not null)
{
Expand Down
1 change: 1 addition & 0 deletions test/GenerationSandbox.Tests/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ ShellWindowTypeConstants
SHFILEOPSTRUCTW
SIZE
SYSTEM_INFO
TrySubmitThreadpoolCallback
VARDESC
WER_REPORT_INFORMATION
wglGetProcAddress
Expand Down
52 changes: 52 additions & 0 deletions test/GenerationSandbox.Tests/ThreadpoolCallbackTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Threading;

[Trait("WindowsOnly", "true")]
public class ThreadpoolCallbackTests
{
// Regression tests for https://github.com/microsoft/CsWin32/issues/1739.
// TP_CALLBACK_ENVIRON_V3 is non-blittable (it contains delegate fields), so in the default
// marshaling mode CsWin32 exposes the optional pcbe parameter as a nullable value type and
// forwards it to the native method through an array: a non-null value becomes a single-element
// array and null becomes a null array. These tests exercise both paths end to end by actually
// submitting work to the native threadpool and confirming the callback runs. Passing null must
// marshal to a null pointer rather than dereferencing a null reference (the original bug).
[Fact]
public void NonNullOptionalNonBlittableStructIsMarshaled()
{
TP_CALLBACK_ENVIRON_V3 environment = default;
environment.Version = 3;
environment.CallbackPriority = TP_CALLBACK_PRIORITY.TP_CALLBACK_PRIORITY_NORMAL;
environment.Size = (uint)Marshal.SizeOf<TP_CALLBACK_ENVIRON_V3>();

AssertCallbackRuns(environment);
}

[Fact]
public void NullOptionalNonBlittableStructDoesNotThrow()
{
AssertCallbackRuns(null);
}

private static unsafe void AssertCallbackRuns(TP_CALLBACK_ENVIRON_V3? environment)
{
using ManualResetEventSlim callbackRan = new(false);
PTP_SIMPLE_CALLBACK callback = (instance, context) => callbackRan.Set();

try
{
BOOL submitted = PInvoke.TrySubmitThreadpoolCallback(callback, environment);
Assert.True(submitted, $"TrySubmitThreadpoolCallback failed with error 0x{Marshal.GetLastWin32Error():X}.");
Assert.True(callbackRan.Wait(TimeSpan.FromSeconds(30), TestContext.Current.CancellationToken), "The threadpool callback did not run.");
}
finally
{
GC.KeepAlive(callback);
}
}
}
Loading