Skip to content

Commit d1c8bbb

Browse files
authored
Generate struct wrapper around function pointer to make a native delegate typedef (#1545)
* Add struct wrapper around function pointer * Only generate the delegate when specifically asked. Otherwise leave as function pointer.
1 parent 9815c14 commit d1c8bbb

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed

src/Microsoft.Windows.CsWin32/Generator.Delegate.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,61 @@ private FunctionPointerParameterSyntax TranslateDelegateToFunctionPointer(TypeHa
151151

152152
return FunctionPointerParameter(parameterTypeInfo.ToTypeSyntax(this.functionPointerTypeSettings, GeneratingElement.FunctionPointer, customAttributeHandles?.QualifyWith(this)).GetUnmarshaledType());
153153
}
154+
155+
// Generates an unsafe readonly struct that wraps the native function pointer for a delegate type definition.
156+
// Structure:
157+
// unsafe readonly struct <Name> { public readonly delegate* unmanaged<...> Value; public <Name>(delegate* unmanaged<...> value) => Value = value; public bool IsNull => Value is null; implicit conversions }
158+
private StructDeclarationSyntax DeclareTypeDefStructForNativeFunctionPointer(TypeDefinition typeDef, Context context)
159+
{
160+
// Delegates are generally managed types, except when using source generators when they never are.
161+
bool isManagedType = !this.UseSourceGenerators;
162+
string name = this.GetMangledIdentifier(this.Reader.GetString(typeDef.Name), context.AllowMarshaling, isManagedType);
163+
164+
FunctionPointerTypeSyntax fpType = this.FunctionPointer(typeDef);
165+
166+
// public readonly delegate* unmanaged<...> Value;
167+
FieldDeclarationSyntax valueField = FieldDeclaration(
168+
VariableDeclaration(fpType.WithTrailingTrivia(TriviaList(Space)))
169+
.AddVariables(VariableDeclarator(Identifier("Value"))))
170+
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.ReadOnlyKeyword));
171+
172+
// public <Name>(delegate* unmanaged<...> value) => Value = value;
173+
ConstructorDeclarationSyntax ctor = ConstructorDeclaration(Identifier(name))
174+
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword))
175+
.AddParameterListParameters(Parameter(Identifier("value")).WithType(fpType))
176+
.WithExpressionBody(ArrowExpressionClause(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName("Value"), IdentifierName("value"))))
177+
.WithSemicolonToken(SemicolonWithLineFeed);
178+
179+
// public static <Name> Null => default;
180+
PropertyDeclarationSyntax nullProperty = PropertyDeclaration(IdentifierName(name).WithTrailingTrivia(Space), Identifier("Null"))
181+
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword))
182+
.WithExpressionBody(ArrowExpressionClause(LiteralExpression(SyntaxKind.DefaultLiteralExpression)))
183+
.WithSemicolonToken(SemicolonWithLineFeed);
184+
185+
// public bool IsNull => Value is null;
186+
PropertyDeclarationSyntax isNullProperty = PropertyDeclaration(PredefinedType(TokenWithSpace(SyntaxKind.BoolKeyword)), Identifier("IsNull"))
187+
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword))
188+
.WithExpressionBody(ArrowExpressionClause(IsPatternExpression(IdentifierName("Value"), ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression)))))
189+
.WithSemicolonToken(SemicolonWithLineFeed);
190+
191+
// public static implicit operator <Name>(delegate* unmanaged<...> value) => new(value);
192+
ConversionOperatorDeclarationSyntax implicitFromPointer = ConversionOperatorDeclaration(Token(SyntaxKind.ImplicitKeyword), IdentifierName(name))
193+
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword))
194+
.AddParameterListParameters(Parameter(Identifier("value")).WithType(fpType))
195+
.WithExpressionBody(ArrowExpressionClause(ObjectCreationExpression(IdentifierName(name)).AddArgumentListArguments(Argument(IdentifierName("value")))))
196+
.WithSemicolonToken(SemicolonWithLineFeed);
197+
198+
// public static implicit operator delegate* unmanaged<...>(<Name> value) => value.Value;
199+
ConversionOperatorDeclarationSyntax implicitToPointer = ConversionOperatorDeclaration(Token(SyntaxKind.ImplicitKeyword), fpType)
200+
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword))
201+
.AddParameterListParameters(Parameter(Identifier("value")).WithType(IdentifierName(name).WithTrailingTrivia(Space)))
202+
.WithExpressionBody(ArrowExpressionClause(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName("value"), IdentifierName("Value"))))
203+
.WithSemicolonToken(SemicolonWithLineFeed);
204+
205+
StructDeclarationSyntax result = StructDeclaration(Identifier(name))
206+
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword), TokenWithSpace(SyntaxKind.ReadOnlyKeyword))
207+
.AddMembers(valueField, ctor, nullProperty, isNullProperty, implicitFromPointer, implicitToPointer);
208+
209+
return result;
210+
}
154211
}

src/Microsoft.Windows.CsWin32/Generator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,8 +1483,9 @@ private IReadOnlyList<ISymbol> FindTypeSymbolsIfAlreadyAvailable(string fullyQua
14831483
{
14841484
typeDeclaration =
14851485
this.IsUntypedDelegate(typeDef) ? this.DeclareUntypedDelegate(typeDef) :
1486-
(this.options.AllowMarshaling && !this.useSourceGenerators) ? this.DeclareDelegate(typeDef) :
1487-
null;
1486+
context.AllowMarshaling ?
1487+
(this.useSourceGenerators ? this.DeclareTypeDefStructForNativeFunctionPointer(typeDef, context) : this.DeclareDelegate(typeDef)) :
1488+
this.DeclareTypeDefStructForNativeFunctionPointer(typeDef, context);
14881489
}
14891490
else
14901491
{

src/Microsoft.Windows.CsWin32/HandleTypeHandleInfo.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ internal override TypeSyntaxAndMarshaling ToTypeSyntax(TypeSyntaxSettings inputs
178178
}
179179
else if ((!inputs.AllowMarshaling || useComSourceGenerators) && isDelegate && inputs.Generator is object && !Generator.IsUntypedDelegate(delegateDefinition.Generator.Reader, delegateDefinition.Definition))
180180
{
181+
// We would like to offer function pointers wrapped in structs (similar to the managed marshalling experience) but that
182+
// makes them worse to work with. See https://github.com/dotnet/roslyn/issues/81234 and https://github.com/microsoft/CsWin32/issues/1542.
183+
// If that ever gets better in C#, we can revisit this. If devs request the delegate by name (e.g. WNDPROC) then we will
184+
// generate the struct and it does implicitly cast down to the function pointer.
181185
return new TypeSyntaxAndMarshaling(inputs.Generator.FunctionPointer(delegateDefinition));
182186
}
183187
else

test/CsWin32Generator.Tests/CsWin32GeneratorTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ public async Task PointerReturnValueIsPreserved()
169169
Assert.Contains("winmdroot.Security.WinTrust.CRYPT_PROVIDER_DATA*", methodReturnTypes);
170170
}
171171

172+
[Fact]
173+
public async Task DelegatesGetStructsGenerated()
174+
{
175+
this.nativeMethods.Add("TIMERPROC");
176+
await this.InvokeGeneratorAndCompileFromFact();
177+
178+
var timerProcType = Assert.Single(this.FindGeneratedType("TIMERPROC"));
179+
}
180+
172181
public static IList<object[]> TestSignatureData => [
173182
["IMFMediaKeySession", "get_KeySystem", "winmdroot.Foundation.BSTR* keySystem"],
174183
["AddPrinterW", "AddPrinter", "string pName, uint Level, Span<byte> pPrinter"],

0 commit comments

Comments
 (0)