Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function pointer calling convention IDE support #59062

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -2189,5 +2189,36 @@ public async Task TestStringEscape(TestHost testHost)
Verbatim("\""),
Punctuation.Semicolon);
}

[Theory]
[CombinatorialData]
[WorkItem(59052, "https://github.com/dotnet/roslyn/issues/59052")]
public async Task FunctionPointerCallingConventions(TestHost testHost)
{
await TestAsync(@"
public unsafe class C
{
delegate* unmanaged[Stdcall]<void> f0;
}
",
testHost,
Keyword("public"),
Keyword("unsafe"),
Keyword("class"),
Class("C"),
Punctuation.OpenCurly,
Keyword("delegate"),
Operators.Asterisk,
Keyword("unmanaged"),
Punctuation.OpenBracket,
Class("Stdcall"),
Punctuation.CloseBracket,
Punctuation.OpenAngle,
Keyword("void"),
Punctuation.CloseAngle,
Field("f0"),
Punctuation.Semicolon,
Punctuation.CloseCurly);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3333,5 +3333,23 @@ $$

Test(workspace, expectedResult:=False)
End Sub

<WpfFact, Trait(Traits.Feature, Traits.Features.GoToDefinition)>
<WorkItem(59052, "https://github.com/dotnet/roslyn/issues/59052")>
Public Sub FunctionPointerCallingConvention()
Dim workspace =
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document><![CDATA[
public unsafe class C
{
delegate* unmanaged[St$$dcall]<void> f0;
}
]]></Document>
</Project>
</Workspace>

Test(workspace)
End Sub
End Class
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public CSharpSyntaxClassificationService(HostLanguageServices languageServices)
new OperatorOverloadSyntaxClassifier(),
new SyntaxTokenClassifier(),
new UsingDirectiveSyntaxClassifier(),
new DiscardSyntaxClassifier()
new DiscardSyntaxClassifier(),
new FunctionPointerCallingConventionClassifier(),
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Classification.Classifiers;
using Microsoft.CodeAnalysis.CSharp.LanguageServices;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.Classification
{
internal class FunctionPointerCallingConventionClassifier : AbstractSyntaxClassifier
{
public override ImmutableArray<Type> SyntaxNodeTypes { get; } = ImmutableArray.Create(
typeof(FunctionPointerUnmanagedCallingConventionSyntax));

public override void AddClassifications(
SyntaxNode syntax,
SemanticModel semanticModel,
ClassificationOptions options,
ArrayBuilder<ClassifiedSpan> result,
CancellationToken cancellationToken)
{
var callingConventionSyntax = (FunctionPointerUnmanagedCallingConventionSyntax)syntax;
if (CSharpSyntaxFacts.Instance.IsSpecialUnmanagedCallingConvention(callingConventionSyntax))
{
result.Add(new ClassifiedSpan(callingConventionSyntax.Span, ClassificationTypeNames.ClassName));
Copy link
Contributor

@Sergio0694 Sergio0694 Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned on Discord, this might actually be more appropriate to be highlighted as a keyword rather than a class, given the compiler is not looking up any type, and there's no associated type symbol here. As discussed with Fred, these identifiers here are effectively "a sort of contextual keyword". Related, we might want to update the coloring for this case in the tooltip as well.

Copy link
Member Author

@Youssef1313 Youssef1313 Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyrusNajmabadi What do you think about classifying as a keyword?

Also I've been looking about the logic that handles function pointers in quick info but can't find where it's. Can you point me out? (specifically, going to definition from quick info already works if only if the compiler is using the type from corlib - where is this logic?) (I think I figured out this. The question is whether it should be classified as a keyword).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am fine with this being classified as a class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that incorrect here? As per our conversation with Fred yesterday (who said he'd be fine with these being classified as a keyword), those are more akin to keywords than classes, given the compiler does not look the types at all. If we classified those as classes, we'd both (1) have an inconsistency in that no symbol info would be available (eg. in quick info), and (2) not actually mirror what the compiler is doing at all, as it's not actually using or looking up those types at all. According to the ECMA spec those calling conventions here are just special symbols, not classes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that incorrect here?

This is classification. There is no concept of 'correctness' :)

According to the ECMA spec those calling conventions here are just special symbols, not classes.

That's fine. I'm still ok with it being classified in this fashion. Classification is not hte compiler. It's whatever we want it to be.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"You keep attacking me as if you think I'm saying let's do a worse solution"

It was absolutely not my intention to attack you (I mean why would I ever want to do that Cyrus?), so I'm really sorry if my messages came across that way. I was just genuinely confused about why the proposed changes were being an issue for you, is all. I do recognize that you just had a different view on the topic, and I was actively trying to understand that better. Nothing of what I said was ever meant to be an attack towards you at all, same as with all other times we happened to disagree on things before as well, of course. I do understand your point better now that you've listed those specific aspects related to complexity and maintainability 🙂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It keeps compiler and ide classification in sync.

If this point is essential, then I think the key point to get to an agreement is to first define what's the expected output of the compiler API (especially that it has effect on the public ToDisplayParts method).


You keep attacking me as of you think I'm saying let's do a worse solution

Absolutely sorry if it felt that way, I definitely didn't mean that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, 'attack' was too strong a word. I likely wasn't making my thoughts very clear (i tend to be juggling like 15 things at at a time :)).

If this point is essential, then I think the key point to get to an agreement is to first define what's the expected output of the compiler API (especially that it has effect on the public ToDisplayParts method).

Yes, i agree with that. If the compiler goes that way, i'm ok keepign IDE in sync.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave you decide which approach is the best, just happy we could clarify the misunderstanding here ❤️

Copy link
Member

@333fred 333fred Jan 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler will not give a type for a single word of Stdcall, Thiscall, Cdecl, or Fastcall. Regardless of whether we decide to return symbols for other combinations of those those words or other calling conventions, we specifically do not look at symbols for those cases. They do not correspond to types and are not emitted as modreqs on the function pointer.

return;
}

var type = semanticModel.Compilation.UnmanagedCallingConventionType(callingConventionSyntax.Name.ValueText);
if (type is not null)
{
result.Add(new ClassifiedSpan(callingConventionSyntax.Name.Span, ClassificationTypeNames.ClassName));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,27 @@ public ImmutableArray<ISymbol> GetBestOrAllSymbols(SemanticModel semanticModel,
{
AssignmentExpressionSyntax _ when token.Kind() == SyntaxKind.EqualsToken => GetDeconstructionAssignmentMethods(semanticModel, node).As<ISymbol>(),
ForEachVariableStatementSyntax _ when token.Kind() == SyntaxKind.InKeyword => GetDeconstructionForEachMethods(semanticModel, node).As<ISymbol>(),
FunctionPointerUnmanagedCallingConventionSyntax callingConvention => GetCallingConventionSymbol(semanticModel, callingConvention),
_ => GetSymbolInfo(semanticModel, node, token, cancellationToken).GetBestOrAllSymbols(),
};
}

private static ImmutableArray<ISymbol> GetCallingConventionSymbol(SemanticModel model, FunctionPointerUnmanagedCallingConventionSyntax syntax)
{
if (CSharpSyntaxFacts.Instance.IsSpecialUnmanagedCallingConvention(syntax))
{
return ImmutableArray<ISymbol>.Empty;
}

var type = model.Compilation.UnmanagedCallingConventionType(syntax.Name.ValueText);
if (type is null)
{
return ImmutableArray<ISymbol>.Empty;
}

return ImmutableArray.Create<ISymbol>(type);
}

private static SymbolInfo GetSymbolInfo(SemanticModel semanticModel, SyntaxNode node, SyntaxToken token, CancellationToken cancellationToken)
{
switch (node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,17 @@ public bool IsVerbatimInterpolatedStringExpression(SyntaxNode node)
=> node is InterpolatedStringExpressionSyntax interpolatedString &&
interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken);

public bool IsSpecialUnmanagedCallingConvention(FunctionPointerUnmanagedCallingConventionSyntax syntax)
{
if (syntax.Parent is not FunctionPointerUnmanagedCallingConventionListSyntax list)
{
return false;
}

return list.CallingConventions.Count == 1 &&
syntax.Name.ValueText is "Cdecl" or "Stdcall" or "Thiscall" or "Fastcall");
}

#region IsXXX members

public bool IsAnonymousFunctionExpression([NotNullWhen(true)] SyntaxNode? node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,11 @@ public static ImmutableArray<IAssemblySymbol> GetReferencedAssemblySymbols(this

public static INamedTypeSymbol? DisallowNullAttribute(this Compilation compilation)
=> compilation.GetTypeByMetadataName(typeof(DisallowNullAttribute).FullName!);

public static INamedTypeSymbol? UnmanagedCallingConventionType(this Compilation compilation, string callConv)
{
var corLibrary = compilation.GetSpecialType(SpecialType.System_Object).ContainingAssembly;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Is this a requirement?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sharwell At compiler-side, yes.

specifierType = compilation.Assembly.CorLibrary.LookupTopLevelMetadataType(ref metadataName, digThroughForwardedTypes: false);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember @jkoritzinsky mentioning how this API could be particularly slow, so much so they tried to avoid using it entirely in the COM generators. Is that still a concern (and if so, should we consider using an alternative here), or has that been fixed now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sergio0694 To my knowledge GetSpecialType is fast, and internally it should be calculated once, and then the lookup is a simple fast array index (not even a Dictionary<TKey, TValue>).

Not sure how it caused issues in generators. I'd love to know more about that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a perf bug in Roslyn that made this call very slow a few months back. It's fixed now.

return corLibrary.GetTypeByMetadataName("System.Runtime.CompilerServices.CallConv" + callConv);
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}