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

Enable APIs using synthesized types to be used on net35 #415

Merged
merged 9 commits into from
Nov 1, 2021
92 changes: 68 additions & 24 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ public class Generator : IDisposable
/// <exception cref=""ArgumentOutOfRangeException"">
/// Thrown when <paramref name=""length""/> is less than <c>0</c> or greater than <see cref=""Length""/>.
/// </exception>
");

private static readonly SyntaxTriviaList StrAsSpanComment = ParseLeadingTrivia(@"/// <summary>
/// Returns a span of the characters in this string.
/// </summary>
");

private static readonly XmlTextSyntax DocCommentStart = XmlText(" ").WithLeadingTrivia(DocumentationCommentExterior("///"));
Expand Down Expand Up @@ -288,6 +293,7 @@ public class Generator : IDisposable
private readonly GeneratorOptions options;
private readonly CSharpCompilation? compilation;
private readonly CSharpParseOptions? parseOptions;
private readonly bool canUseSpan;
private readonly bool canCallCreateSpan;
private readonly bool getDelegateForFunctionPointerGenericExists;
private readonly bool generateSupportedOSPlatformAttributes;
Expand Down Expand Up @@ -318,6 +324,7 @@ public Generator(string metadataLibraryPath, Docs? docs, GeneratorOptions option
this.parseOptions = parseOptions;
this.volatileCode = new(this.committedCode);

this.canUseSpan = this.compilation?.GetTypeByMetadataName(typeof(Span<>).FullName) is not null;
this.canCallCreateSpan = this.compilation?.GetTypeByMetadataName(typeof(MemoryMarshal).FullName)?.GetMembers("CreateSpan").Any() is true;
this.getDelegateForFunctionPointerGenericExists = this.compilation?.GetTypeByMetadataName(typeof(Marshal).FullName)?.GetMembers(nameof(Marshal.GetDelegateForFunctionPointer)).Any(m => m is IMethodSymbol { IsGenericMethod: true }) is true;
this.generateDefaultDllImportSearchPathsAttribute = this.compilation?.GetTypeByMetadataName(typeof(DefaultDllImportSearchPathsAttribute).FullName) is object;
Expand Down Expand Up @@ -849,6 +856,14 @@ public bool TryGenerateType(string possiblyQualifiedName, out IReadOnlyList<stri
return false;
}

if (SpecialTypeDefNames.Contains(typeName))
AArnott marked this conversation as resolved.
Show resolved Hide resolved
{
string? fullyQualifiedName = null;
this.volatileCode.GenerationTransaction(() => this.RequestSpecialTypeDefStruct(typeName, out fullyQualifiedName));
preciseApi = ImmutableList.Create(fullyQualifiedName!);
return true;
}

if (foundApiWithMismatchedPlatform)
{
throw new PlatformIncompatibleException($"The requested API ({possiblyQualifiedName}) was found but is not available given the target platform ({this.compilation?.Options.Platform}).");
Expand Down Expand Up @@ -1600,10 +1615,26 @@ internal void GetBaseTypeInfo(TypeDefinition typeDef, out StringHandle baseTypeN
{
case "PCWSTR":
specialDeclaration = this.FetchTemplate($"{specialName}");

if (this.canUseSpan)
{
// internal ReadOnlySpan<char> AsSpan() => this.Value is null ? default(ReadOnlySpan<char>) : new ReadOnlySpan<char>(this.Value, this.Length);
specialDeclaration = ((TypeDeclarationSyntax)specialDeclaration).AddMembers(
this.CreateAsSpanMethodOverValueAndLength(MakeReadOnlySpanOfT(PredefinedType(Token(SyntaxKind.CharKeyword)))));
}

this.TryGenerateType("PWSTR"); // the template references this type
break;
case "PCSTR":
specialDeclaration = this.FetchTemplate($"{specialName}");

if (this.canUseSpan)
{
// internal ReadOnlySpan<byte> AsSpan() => this.Value is null ? default(ReadOnlySpan<byte>) : new ReadOnlySpan<byte>(this.Value, this.Length);
specialDeclaration = ((TypeDeclarationSyntax)specialDeclaration).AddMembers(
this.CreateAsSpanMethodOverValueAndLength(MakeReadOnlySpanOfT(PredefinedType(Token(SyntaxKind.ByteKeyword)))));
}

this.TryGenerateType("PSTR"); // the template references this type
break;
default:
Expand Down Expand Up @@ -3697,16 +3728,28 @@ private IEnumerable<MemberDeclarationSyntax> CreateAdditionalTypeDefPWSTRMembers
.AddArgumentListArguments(Argument(thisValue)))))
.WithSemicolonToken(SemicolonWithLineFeed);

// internal Span<char> AsSpan() => this.Value is null ? default : new Span<char>(this.Value, this.Length);
TypeSyntax spanChar = MakeSpanOfT(PredefinedType(Token(SyntaxKind.CharKeyword)));
if (this.canUseSpan)
{
// internal Span<char> AsSpan() => this.Value is null ? default : new Span<char>(this.Value, this.Length);
yield return this.CreateAsSpanMethodOverValueAndLength(MakeSpanOfT(PredefinedType(Token(SyntaxKind.CharKeyword))));
}
#pragma warning restore SA1114 // Parameter list should follow declaration
}

private MethodDeclarationSyntax CreateAsSpanMethodOverValueAndLength(TypeSyntax spanType)
{
ExpressionSyntax thisValue = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("Value"));
ExpressionSyntax thisLength = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("Length"));
ExpressionSyntax spanCreation = ObjectCreationExpression(spanChar).AddArgumentListArguments(Argument(thisValue), Argument(thisLength));
ExpressionSyntax conditional = ConditionalExpression(thisValueIsNull, DefaultExpression(spanChar), spanCreation);
yield return MethodDeclaration(spanChar, Identifier("AsSpan"))

// internal X AsSpan() => this.Value is null ? default(X) : new X(this.Value, this.Length);
return MethodDeclaration(spanType, Identifier("AsSpan"))
.AddModifiers(TokenWithSpace(this.Visibility))
.WithExpressionBody(ArrowExpressionClause(conditional))
.WithSemicolonToken(SemicolonWithLineFeed);
#pragma warning restore SA1114 // Parameter list should follow declaration
.WithExpressionBody(ArrowExpressionClause(ConditionalExpression(
condition: IsPatternExpression(thisValue, ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression))),
whenTrue: DefaultExpression(spanType),
whenFalse: ObjectCreationExpression(spanType).AddArgumentListArguments(Argument(thisValue), Argument(thisLength)))))
.WithSemicolonToken(SemicolonWithLineFeed)
.WithLeadingTrivia(StrAsSpanComment);
}

private StructDeclarationSyntax DeclareTypeDefBOOLStruct(TypeDefinition typeDef)
Expand All @@ -3732,21 +3775,17 @@ private StructDeclarationSyntax DeclareTypeDefBOOLStruct(TypeDefinition typeDef)
.WithExpressionBody(ArrowExpressionClause(fieldAccessExpression)).WithSemicolonToken(SemicolonWithLineFeed)
.AddModifiers(TokenWithSpace(this.Visibility)));

static InvocationExpressionSyntax UnsafeAs(SyntaxKind fromType, SyntaxKind toType, IdentifierNameSyntax localSource) =>
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(nameof(Unsafe)),
GenericName(nameof(Unsafe.As), TypeArgumentList().AddArguments(PredefinedType(Token(fromType)), PredefinedType(Token(toType))))),
ArgumentList().AddArguments(Argument(localSource).WithRefKindKeyword(Token(SyntaxKind.RefKeyword))));

// BOOL(bool value) => this.value = Unsafe.As<bool, sbyte>(ref value);
// unsafe BOOL(bool value) => this.value = *(sbyte*)&value;
IdentifierNameSyntax valueParameter = IdentifierName("value");
ExpressionSyntax boolToInt = UnsafeAs(SyntaxKind.BoolKeyword, SyntaxKind.SByteKeyword, valueParameter);
ExpressionSyntax boolToSByte = PrefixUnaryExpression(
SyntaxKind.PointerIndirectionExpression,
CastExpression(
PointerType(PredefinedType(TokenWithNoSpace(SyntaxKind.SByteKeyword))),
PrefixUnaryExpression(SyntaxKind.AddressOfExpression, valueParameter)));
members = members.Add(ConstructorDeclaration(name.Identifier)
.AddModifiers(TokenWithSpace(this.Visibility))
.AddModifiers(TokenWithSpace(this.Visibility), TokenWithSpace(SyntaxKind.UnsafeKeyword))
.AddParameterListParameters(Parameter(valueParameter.Identifier).WithType(PredefinedType(TokenWithSpace(SyntaxKind.BoolKeyword))))
.WithExpressionBody(ArrowExpressionClause(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, fieldAccessExpression, boolToInt).WithOperatorToken(TokenWithSpaces(SyntaxKind.EqualsToken))))
.WithExpressionBody(ArrowExpressionClause(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, fieldAccessExpression, boolToSByte).WithOperatorToken(TokenWithSpaces(SyntaxKind.EqualsToken))))
.WithSemicolonToken(SemicolonWithLineFeed));

// BOOL(int value) => this.value = value;
Expand All @@ -3756,20 +3795,25 @@ static InvocationExpressionSyntax UnsafeAs(SyntaxKind fromType, SyntaxKind toTyp
.WithExpressionBody(ArrowExpressionClause(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, fieldAccessExpression, valueParameter).WithOperatorToken(TokenWithSpaces(SyntaxKind.EqualsToken))))
.WithSemicolonToken(SemicolonWithLineFeed));

// public static implicit operator bool(BOOL value)
// public unsafe static implicit operator bool(BOOL value)
// {
// sbyte v = checked((sbyte)value.value);
// return Unsafe.As<sbyte, bool>(ref v);
// return *(bool*)&v;
// }
IdentifierNameSyntax localVarName = IdentifierName("v");
ExpressionSyntax sbyteToBool = PrefixUnaryExpression(
SyntaxKind.PointerIndirectionExpression,
CastExpression(
PointerType(PredefinedType(TokenWithNoSpace(SyntaxKind.BoolKeyword))),
PrefixUnaryExpression(SyntaxKind.AddressOfExpression, localVarName)));
var implicitBOOLtoBoolBody = Block().AddStatements(
LocalDeclarationStatement(VariableDeclaration(PredefinedType(Token(SyntaxKind.SByteKeyword)))).AddDeclarationVariables(
VariableDeclarator(localVarName.Identifier).WithInitializer(EqualsValueClause(CheckedExpression(SyntaxKind.CheckedExpression, CastExpression(PredefinedType(Token(SyntaxKind.SByteKeyword)), MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, valueParameter, fieldName)))))),
ReturnStatement(UnsafeAs(SyntaxKind.SByteKeyword, SyntaxKind.BoolKeyword, localVarName)));
ReturnStatement(sbyteToBool));
members = members.Add(ConversionOperatorDeclaration(Token(SyntaxKind.ImplicitKeyword), PredefinedType(Token(SyntaxKind.BoolKeyword)))
.AddParameterListParameters(Parameter(valueParameter.Identifier).WithType(name.WithTrailingTrivia(TriviaList(Space))))
.WithBody(implicitBOOLtoBoolBody)
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword))); // operators MUST be public
.AddModifiers(TokenWithSpace(SyntaxKind.PublicKeyword), TokenWithSpace(SyntaxKind.StaticKeyword), TokenWithSpace(SyntaxKind.UnsafeKeyword))); // operators MUST be public

// public static implicit operator BOOL(bool value) => new BOOL(value);
members = members.Add(ConversionOperatorDeclaration(Token(SyntaxKind.ImplicitKeyword), name)
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.Windows.CsWin32/templates/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.cs]
indent_style = tab
5 changes: 0 additions & 5 deletions src/Microsoft.Windows.CsWin32/templates/PCSTR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,5 @@ internal int Length
/// <returns>A <see langword="string"/>, or <see langword="null"/> if <see cref="Value"/> is <see langword="null"/>.</returns>
public override string ToString() => this.Value is null ? null : new string((sbyte*)this.Value, 0, this.Length, global::System.Text.Encoding.UTF8);

/// <summary>
/// Returns a span of the characters in this string.
/// </summary>
internal ReadOnlySpan<byte> AsSpan() => this.Value is null ? default(ReadOnlySpan<byte>) : new ReadOnlySpan<byte>(this.Value, this.Length);

private string DebuggerDisplay => this.ToString();
}
5 changes: 0 additions & 5 deletions src/Microsoft.Windows.CsWin32/templates/PCWSTR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,5 @@ internal int Length
/// <returns>A <see langword="string"/>, or <see langword="null"/> if <see cref="Value"/> is <see langword="null"/>.</returns>
public override string ToString() => this.Value is null ? null : new string(this.Value);

/// <summary>
/// Returns a span of the characters in this string.
/// </summary>
internal ReadOnlySpan<char> AsSpan() => this.Value is null ? default(ReadOnlySpan<char>) : new ReadOnlySpan<char>(this.Value, this.Length);

private string DebuggerDisplay => this.ToString();
}
68 changes: 52 additions & 16 deletions test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ public GeneratorTests(ITestOutputHelper logger)
public static IEnumerable<object[]> TFMData =>
new object[][]
{
new object[] { "net40" },
new object[] { "net35" },
new object[] { "netstandard2.0" },
new object[] { "net5.0" },
};

public async Task InitializeAsync()
{
this.starterCompilations.Add("net40", await this.CreateCompilationAsync(MyReferenceAssemblies.NetFramework.Net40));
this.starterCompilations.Add("net35", await this.CreateCompilationAsync(MyReferenceAssemblies.NetFramework.Net35));
this.starterCompilations.Add("netstandard2.0", await this.CreateCompilationAsync(MyReferenceAssemblies.NetStandard20));
this.starterCompilations.Add("net5.0", await this.CreateCompilationAsync(MyReferenceAssemblies.Net.Net50));
this.starterCompilations.Add("net5.0-x86", await this.CreateCompilationAsync(MyReferenceAssemblies.Net.Net50, Platform.X86));
Expand Down Expand Up @@ -112,7 +112,7 @@ public void SimplestMethod(string tfm)
Assert.DoesNotContain(generatedMethod.AttributeLists, al => IsAttributePresent(al, "SupportedOSPlatform"));
}

if (tfm != "net40")
if (tfm != "net35")
{
Assert.Contains(generatedMethod.AttributeLists, al => IsAttributePresent(al, "DefaultDllImportSearchPaths"));
}
Expand Down Expand Up @@ -791,6 +791,40 @@ internal enum FILE_CREATE_FLAGS
this.AssertNoDiagnostics();
}

[Theory]
[InlineData("BOOL")]
[InlineData("HRESULT")]
[InlineData("NTSTATUS")]
[InlineData("PCSTR")]
[InlineData("PCWSTR")]
[InlineData("PWSTR")]
public void SynthesizedTypesCanBeDirectlyRequested(string synthesizedTypeName)
{
this.generator = this.CreateGenerator();
Assert.True(this.generator.TryGenerate(synthesizedTypeName, CancellationToken.None));
this.CollectGeneratedCode(this.generator);
this.AssertNoDiagnostics();
Assert.Single(this.FindGeneratedType(synthesizedTypeName));
}

[Theory]
[InlineData("BOOL")]
[InlineData("HRESULT")]
[InlineData("NTSTATUS")]
[InlineData("PCSTR")]
[InlineData("PCWSTR")]
[InlineData("PWSTR")]
public void SynthesizedTypesWorkInNet35(string synthesizedTypeName)
{
this.compilation = this.starterCompilations["net35"];
this.generator = this.CreateGenerator();

Assert.True(this.generator.TryGenerate(synthesizedTypeName, CancellationToken.None));
this.CollectGeneratedCode(this.generator);
this.AssertNoDiagnostics();
Assert.Single(this.FindGeneratedType(synthesizedTypeName));
}

/// <summary>
/// Validates that where MemoryMarshal.CreateSpan isn't available, a substitute indexer is offered.
/// </summary>
Expand Down Expand Up @@ -1087,12 +1121,12 @@ internal readonly partial struct BOOL
private readonly int value;

internal int Value => this.value;
internal BOOL(bool value) => this.value = Unsafe.As<bool,sbyte>(ref value);
internal unsafe BOOL(bool value) => this.value = *(sbyte*)&value;
internal BOOL(int value) => this.value = value;
public static implicit operator bool(BOOL value)
public static unsafe implicit operator bool(BOOL value)
{
sbyte v = checked((sbyte)value.value);
return Unsafe.As<sbyte,bool>(ref v);
return *(bool*)&v;
}
public static implicit operator BOOL(bool value) => new BOOL(value);
public static explicit operator BOOL(int value) => new BOOL(value);
Expand Down Expand Up @@ -1344,12 +1378,12 @@ internal readonly partial struct BOOL
private readonly int value;

internal int Value => this.value;
internal BOOL(bool value) => this.value = Unsafe.As<bool,sbyte>(ref value);
internal unsafe BOOL(bool value) => this.value = *(sbyte*)&value;
internal BOOL(int value) => this.value = value;
public static implicit operator bool(BOOL value)
public static unsafe implicit operator bool(BOOL value)
{
sbyte v = checked((sbyte)value.value);
return Unsafe.As<sbyte,bool>(ref v);
return *(bool*)&v;
}
public static implicit operator BOOL(bool value) => new BOOL(value);
public static explicit operator BOOL(int value) => new BOOL(value);
Expand Down Expand Up @@ -1654,12 +1688,12 @@ internal readonly partial struct BOOL
private readonly int value;

internal int Value => this.value;
internal BOOL(bool value) => this.value = Unsafe.As<bool,sbyte>(ref value);
internal unsafe BOOL(bool value) => this.value = *(sbyte*)&value;
internal BOOL(int value) => this.value = value;
public static implicit operator bool(BOOL value)
public static unsafe implicit operator bool(BOOL value)
{
sbyte v = checked((sbyte)value.value);
return Unsafe.As<sbyte,bool>(ref v);
return *(bool*)&v;
}
public static implicit operator BOOL(bool value) => new BOOL(value);
public static explicit operator BOOL(int value) => new BOOL(value);
Expand Down Expand Up @@ -1963,13 +1997,12 @@ internal int Length
public override string ToString() => this.Value is null ? null : new string(this.Value);


private string DebuggerDisplay => this.ToString();

/// <summary>
/// Returns a span of the characters in this string.
/// </summary>
internal ReadOnlySpan<char> AsSpan() => this.Value is null ? default(ReadOnlySpan<char>) : new ReadOnlySpan<char>(this.Value, this.Length);
jnm2 marked this conversation as resolved.
Show resolved Hide resolved


private string DebuggerDisplay => this.ToString();
}
}
}
Expand Down Expand Up @@ -2132,6 +2165,9 @@ internal int Length

public override string ToString() => this.Value is null ? null : new string(this.Value);

/// <summary>
/// Returns a span of the characters in this string.
/// </summary>
internal Span<char> AsSpan() => this.Value is null ? default(Span<char>) : new Span<char>(this.Value, this.Length);
}
}
Expand Down Expand Up @@ -2432,7 +2468,7 @@ private static class MyReferenceAssemblies

internal static class NetFramework
{
internal static readonly ReferenceAssemblies Net40 = ReferenceAssemblies.NetFramework.Net40.Default.AddPackages(AdditionalPackages);
internal static readonly ReferenceAssemblies Net35 = ReferenceAssemblies.NetFramework.Net35.Default.AddPackages(AdditionalPackages);
}

internal static class Net
Expand Down