Skip to content

Commit

Permalink
Merge pull request #415 from jnm2/templates_on_net35
Browse files Browse the repository at this point in the history
Enable APIs using synthesized types to be used on net35
  • Loading branch information
AArnott authored Nov 1, 2021
2 parents 8b50484 + bc4d5b5 commit 93076e7
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 50 deletions.
92 changes: 68 additions & 24 deletions src/Microsoft.Windows.CsWin32/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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))
{
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 @@ -1599,10 +1614,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 @@ -3718,16 +3749,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 @@ -3753,21 +3796,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 @@ -3777,20 +3816,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 @@ -792,6 +792,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 @@ -1082,12 +1116,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 @@ -1339,12 +1373,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 @@ -1649,12 +1683,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 @@ -1958,13 +1992,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);
private string DebuggerDisplay => this.ToString();
}
}
}
Expand Down Expand Up @@ -2127,6 +2160,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 @@ -2427,7 +2463,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

0 comments on commit 93076e7

Please sign in to comment.