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

Support [GeneratedBindableCustomProperty] on structs and records #1854

Merged
merged 13 commits into from
Oct 30, 2024
47 changes: 35 additions & 12 deletions src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,16 @@ private static bool IsComponentType(SyntaxNode node)

private static bool NeedCustomPropertyImplementation(SyntaxNode node)
{
return node is ClassDeclarationSyntax declaration &&
!declaration.Modifiers.Any(static m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.AbstractKeyword)) &&
GeneratorHelper.IsPartial(declaration) &&
GeneratorHelper.HasBindableCustomPropertyAttribute(declaration);
if ((node is ClassDeclarationSyntax classDeclaration && !classDeclaration.Modifiers.Any(static m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.AbstractKeyword))) ||
(node is RecordDeclarationSyntax recordDeclaration && !recordDeclaration.Modifiers.Any(static m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.AbstractKeyword))) ||
(node is StructDeclarationSyntax structDeclaration && !structDeclaration.Modifiers.Any(static m => m.IsKind(SyntaxKind.StaticKeyword))))
{
TypeDeclarationSyntax typeDeclaration = (TypeDeclarationSyntax)node;

return GeneratorHelper.IsPartial(typeDeclaration) && GeneratorHelper.HasBindableCustomPropertyAttribute(typeDeclaration);
}

return false;
}

private static (VtableAttribute, EquatableArray<VtableAttribute>) GetVtableAttributeToAdd(
Expand Down Expand Up @@ -249,7 +255,7 @@ private static VtableAttribute GetVtableAttributesForTaskAdapters(GeneratorSynta
#nullable enable
private static BindableCustomProperties GetBindableCustomProperties(GeneratorSyntaxContext context)
{
var symbol = context.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)context.Node)!;
var symbol = context.SemanticModel.GetDeclaredSymbol((TypeDeclarationSyntax)context.Node)!;
INamedTypeSymbol bindableCustomPropertyAttributeSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute")!;

if (bindableCustomPropertyAttributeSymbol is null ||
Expand Down Expand Up @@ -334,6 +340,8 @@ private static BindableCustomProperties GetBindableCustomProperties(GeneratorSyn
@namespace,
isGlobalNamespace,
typeName,
symbol.TypeKind,
symbol.IsRecord,
classHierarchy,
ToFullyQualifiedString(symbol),
bindableCustomProperties.ToImmutableArray());
Expand Down Expand Up @@ -1614,7 +1622,7 @@ namespace {{bindableCustomProperties.Namespace}}
""");
}

var escapedClassName = GeneratorHelper.EscapeTypeNameForIdentifier(bindableCustomProperties.ClassName);
var escapedClassName = GeneratorHelper.EscapeTypeNameForIdentifier(bindableCustomProperties.TypeName);

ReadOnlySpan<TypeInfo> classHierarchy = bindableCustomProperties.ClassHierarchy.AsSpan();
// If the type is nested, correctly nest the type definition
Expand All @@ -1626,8 +1634,10 @@ namespace {{bindableCustomProperties.Namespace}}
""");
}

string typeKeyword = TypeInfo.GetTypeKeyword(bindableCustomProperties.TypeKind, bindableCustomProperties.IsRecord);

source.AppendLine($$"""
partial class {{(classHierarchy.IsEmpty ? bindableCustomProperties.ClassName : classHierarchy[0].QualifiedName)}} : global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation
partial {{typeKeyword}} {{(classHierarchy.IsEmpty ? bindableCustomProperties.TypeName : classHierarchy[0].QualifiedName)}} : global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation
{
global::Microsoft.UI.Xaml.Data.BindableCustomProperty global::Microsoft.UI.Xaml.Data.IBindableCustomPropertyImplementation.GetProperty(string name)
{
Expand Down Expand Up @@ -1747,7 +1757,9 @@ internal readonly record struct BindableCustomProperty(
internal readonly record struct BindableCustomProperties(
string Namespace,
bool IsGlobalNamespace,
string ClassName,
string TypeName,
TypeKind TypeKind,
bool IsRecord,
EquatableArray<TypeInfo> ClassHierarchy,
string QualifiedClassName,
EquatableArray<BindableCustomProperty> Properties);
Expand All @@ -1756,7 +1768,7 @@ internal readonly record struct BindableCustomProperties(
/// A model describing a type info in a type hierarchy.
/// </summary>
/// <param name="QualifiedName">The qualified name for the type.</param>
/// <param name="Kind">The type of the type in the hierarchy.</param>
/// <param name="Kind">The kind of the type in the hierarchy.</param>
/// <param name="IsRecord">Whether the type is a record type.</param>
// Ported from https://github.com/Sergio0694/ComputeSharp
internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord)
Expand All @@ -1767,12 +1779,23 @@ internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsReco
/// <returns>The keyword for the current type kind.</returns>
public string GetTypeKeyword()
{
return Kind switch
return GetTypeKeyword(Kind, IsRecord);
}

/// <summary>
/// Gets the keyword for a given kind and record option.
/// </summary>
/// <param name="kind">The type kind.</param>
/// <param name="isRecord">Whether the type is a record.</param>
/// <returns>The keyword for a given kind and record option.</returns>
public static string GetTypeKeyword(TypeKind kind, bool isRecord)
{
return kind switch
{
TypeKind.Struct when IsRecord => "record struct",
TypeKind.Struct when isRecord => "record struct",
TypeKind.Struct => "struct",
TypeKind.Interface => "interface",
TypeKind.Class when IsRecord => "record",
TypeKind.Class when isRecord => "record",
_ => "class"
};
}
Expand Down
8 changes: 8 additions & 0 deletions src/Authoring/WinRT.SourceGenerator/WinRTTypeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,14 @@ private void AddCustomAttributes(IEnumerable<AttributeData> attributes, EntityHa
continue;
}

// Skip the [GeneratedBindableCustomProperty] attribute. It is valid to add this on types in WinRT
// components (if they need to be exposed and implement ICustomPropertyProvider), but the attribute
// should never show up in the .winmd file (it would also cause build errors in the projections).
if (attributeType.ToString() == "WinRT.GeneratedBindableCustomPropertyAttribute")
{
continue;
}

Logger.Log("attribute: " + attribute);
Logger.Log("attribute type: " + attributeType);
Logger.Log("attribute constructor: " + attribute.AttributeConstructor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,9 @@
name="AuthoringTest.CustomPropertyProviderWithExplicitImplementation"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
<activatableClass
name="AuthoringTest.CustomPropertyRecordTypeFactory"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
</file>
</assembly>
54 changes: 54 additions & 0 deletions src/Tests/AuthoringConsumptionTest/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -771,4 +771,58 @@ TEST(AuthoringTest, ExplicitlyImplementedICustomPropertyProvider)

auto propertyValue = customProperty.GetValue(nullptr);
EXPECT_EQ(winrt::unbox_value<hstring>(propertyValue), L"TestPropertyValue");
}

TEST(AuthoringTest, GeneratedCustomPropertyStructType)
{
auto userObject = CustomPropertyRecordTypeFactory::CreateStruct();

// We should be able to cast to 'ICustomPropertyProvider'
auto propertyProvider = userObject.as<Microsoft::UI::Xaml::Data::ICustomPropertyProvider>();

auto customProperty = propertyProvider.GetCustomProperty(L"Value");

EXPECT_NE(customProperty, nullptr);
EXPECT_TRUE(customProperty.CanRead());
EXPECT_FALSE(customProperty.CanWrite());
EXPECT_EQ(customProperty.Name(), L"Value");

auto propertyValue = customProperty.GetValue(userObject);
EXPECT_EQ(winrt::unbox_value<hstring>(propertyValue), L"CsWinRTFromStructType");
}

TEST(AuthoringTest, GeneratedCustomPropertyRecordType)
{
auto userObject = CustomPropertyRecordTypeFactory::CreateRecord();

// We should be able to cast to 'ICustomPropertyProvider'
auto propertyProvider = userObject.as<Microsoft::UI::Xaml::Data::ICustomPropertyProvider>();

auto customProperty = propertyProvider.GetCustomProperty(L"Value");

EXPECT_NE(customProperty, nullptr);
EXPECT_TRUE(customProperty.CanRead());
EXPECT_FALSE(customProperty.CanWrite());
EXPECT_EQ(customProperty.Name(), L"Value");

auto propertyValue = customProperty.GetValue(userObject);
EXPECT_EQ(winrt::unbox_value<hstring>(propertyValue), L"CsWinRTFromRecordType");
}

TEST(AuthoringTest, CustomPropertyRecordStructTypeFactoryAndICPP)
{
auto userObject = CustomPropertyRecordTypeFactory::CreateRecordStruct();

// We should be able to cast to 'ICustomPropertyProvider'
auto propertyProvider = userObject.as<Microsoft::UI::Xaml::Data::ICustomPropertyProvider>();

auto customProperty = propertyProvider.GetCustomProperty(L"Value");

EXPECT_NE(customProperty, nullptr);
EXPECT_TRUE(customProperty.CanRead());
EXPECT_FALSE(customProperty.CanWrite());
EXPECT_EQ(customProperty.Name(), L"Value");

auto propertyValue = customProperty.GetValue(userObject);
EXPECT_EQ(winrt::unbox_value<hstring>(propertyValue), L"CsWinRTFromRecordStructType");
}
33 changes: 33 additions & 0 deletions src/Tests/AuthoringTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,39 @@ public sealed partial class CustomProperty
public string Value => "CsWinRT";
}

[GeneratedBindableCustomProperty]
public partial struct CustomPropertyStructType
{
// Public WinRT struct types must have at least one field
public int Dummy;

public int Number => 4;
public string Value => "CsWinRTFromStructType";
}

[GeneratedBindableCustomProperty]
internal sealed partial record CustomPropertyRecordType
{
public int Number { get; } = 4;
public string Value => "CsWinRTFromRecordType";
}

[GeneratedBindableCustomProperty]
internal partial record struct CustomPropertyRecordStructType
{
public int Number => 4;
public string Value => "CsWinRTFromRecordStructType";
}

public static class CustomPropertyRecordTypeFactory
{
public static object CreateStruct() => new CustomPropertyStructType();

public static object CreateRecord() => new CustomPropertyRecordType();

public static object CreateRecordStruct() => default(CustomPropertyRecordStructType);
}

public sealed partial class CustomPropertyProviderWithExplicitImplementation : ICustomPropertyProvider
{
public Type Type => typeof(CustomPropertyProviderWithExplicitImplementation);
Expand Down
9 changes: 6 additions & 3 deletions src/WinRT.Runtime/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,13 @@ public WinRTAssemblyExportsTypeAttribute(Type type)
}

/// <summary>
/// An attribute used to indicate the properties which are bindable via the <see cref="Microsoft.UI.Xaml.Data.ICustomProperty"/> implementation provided for use in WinUI scenarios.
/// The type which this attribute is placed on also needs to be marked partial and needs to be non-generic.
/// An attribute used to indicate the properties which are bindable via the <see cref="Microsoft.UI.Xaml.Data.ICustomProperty"/> implementation
/// provided for use in WinUI scenarios. The type which this attribute is placed on also needs to be marked partial and needs to be non-generic.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
/// <remarks>
/// This type also provides equivalent support for the UWP XAML interface (as it shares the same IID as the WinUI type).
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
#if EMBED
internal
#else
Expand Down