Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 8 additions & 2 deletions docs/cswinrt3.0-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,15 @@ CsWinRT 3.0 will update the projection of `T[]` parameters to use first-class sp

This means you no longer need to allocate and copy stuff into arrays all over the place (because you also need the size to be exactly right, so you can't even use the array pool). Instead, you'll be able to just use spans normally. Which also means you can now even make it 0-alloc and pass stack-allocated params entirely. This is source compatible thanks to the [first class span](https://learn.microsoft.com/dotnet/csharp/language-reference/proposals/first-class-span-types) types feature of C# 14.

### Fixing `Point`/`Rect`/`Size` properties
### Fixing `Point`/`Rect`/`Size` fields

These foundational types have historically been projecting their fields as `double`, instead of `float`. This is not ideal for several reasons: it introduces implicit casts when assigning to or reading from them, it doesn't match the WinRT ABI (the backing data is still just floats), and it unnecessary impacts performance when doing lots of heavy calculations with them. In CsWinRT 3.0, we want to try fixing this design aspect and correctly projecting these members as `float`, and monitor what the real impact is on popular projects using WinRT from C#.
These foundational types have historically been projecting their fields as `double` properties, instead of `float` fields. This is not ideal for several reasons: it introduces implicit casts when assigning to or reading from them, it doesn't match the WinRT ABI (the backing data is still just floats), and it unnecessary impacts performance when doing lots of heavy calculations with them. In CsWinRT 3.0, we want to try fixing this design aspect and correctly projecting these members as `float` fields, and monitor what the real impact is on popular projects using WinRT from C#.
Comment thread
manodasanW marked this conversation as resolved.

### Projecting struct fields as fields

Windows Runtime structs are plain data: all their members are fields in metadata. CsWinRT 3.0 projects them as C# fields, matching both the metadata and what C++/WinRT does. This keeps the projected shape honest (there is no accessor to run any logic behind), it allows callers to take a reference to a member (e.g. to pass it as a `ref` argument), and it makes authoring work symmetrically: `cswinrtwinmdgen.exe` maps public instance fields of an authored `struct` back to Windows Runtime struct fields, so the shape you consume is exactly the shape you author.

This also applies to the manually projected and custom-mapped struct types, such as `Windows.Foundation.Point` and `Microsoft.UI.Xaml.CornerRadius`. Any additional member those types expose that does not correspond to a Windows Runtime struct field (e.g. `Rect.Left` or `GridLength.IsAuto`) remains a managed-only property.

### Unifying foundational event handers with .NET

Expand Down
2 changes: 1 addition & 1 deletion docs/event-infrastructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ A simple value type wrapping a 64-bit integer. This is the Windows Runtime's con
```csharp
public struct EventRegistrationToken : IEquatable<EventRegistrationToken>
{
public long Value { get; set; }
public long Value;
}
```

Expand Down
55 changes: 55 additions & 0 deletions src/Tests/ProjectionWriterTest/Test_ProjectedStructs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using ProjectionWriterTest.Helpers;

namespace ProjectionWriterTest;

/// <summary>
/// Tests for how the projection writer projects the members of Windows Runtime struct types.
/// </summary>
/// <remarks>
/// Windows Runtime structs are plain data: their members are fields in metadata, and they must be
/// projected as C# fields. Projecting them as properties breaks authoring (the WinMD generator only
/// maps public instance fields back to Windows Runtime struct fields, so an authored struct using
/// properties would produce an empty struct in metadata), and it prevents callers from taking a
/// reference to a member.
/// </remarks>
[TestClass]
public class Test_ProjectedStructs
{
/// <summary>
/// Every field of a projected struct is emitted as a public C# field.
/// </summary>
/// <remarks>
/// <c>Windows.Foundation.Numerics.Rational</c> is the only non-mapped struct in the projected
/// namespace, so it is the anchor for both assertions.
/// </remarks>
[TestMethod]
[DataRow(true)]
[DataRow(false)]
public void StructFields_AreProjectedAsFields(bool referenceProjection)
{
string sources = ProjectionWriterRunner.GetSources(referenceProjection);

StringAssert.Contains(sources, "public uint Numerator;", "'Rational.Numerator' should be projected as a field.");
StringAssert.Contains(sources, "public uint Denominator;", "'Rational.Denominator' should be projected as a field.");
}

/// <summary>
/// No projected struct member is emitted as an auto-property.
/// </summary>
/// <remarks>
/// Guards against a regression to the <c>{ readonly get; set; }</c> form that projected struct
/// members used to be emitted with.
/// </remarks>
[TestMethod]
[DataRow(true)]
[DataRow(false)]
public void StructFields_AreNotProjectedAsProperties(bool referenceProjection)
{
string sources = ProjectionWriterRunner.GetSources(referenceProjection);

Assert.IsFalse(sources.Contains("readonly get; set;"), "Struct members should be projected as fields, not as auto-properties.");
}
}
21 changes: 10 additions & 11 deletions src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@ public partial struct {{projectionName}} : IEquatable<{{projectionName}}>

using (writer.WriteBlock())
{
// Windows Runtime struct fields are projected as C# fields, matching the ABI
// layout exactly. They are emitted first, so that the declaration order (which
// determines the sequential layout of the struct) mirrors the metadata order.
foreach ((string typeStr, string name, string _, bool _) in fields)
{
writer.WriteLine($"public {typeStr} {name};");
}

writer.WriteLineIf(fields.Count > 0);

// Emit the constructor declaration
writer.Write($"public {projectionName}(");
for (int i = 0; i < fields.Count; i++)
Expand Down Expand Up @@ -233,17 +243,6 @@ public partial struct {{projectionName}} : IEquatable<{{projectionName}}>
writer.WriteLine();
}

// Properties (all getters are readonly)
foreach ((string typeStr, string name, string _, bool _) in fields)
{
writer.WriteLine($$"""
public {{typeStr}} {{name}}
{
readonly get; set;
}
""");
}

// Overridden '==' operator
writer.Write($"public static bool operator ==({projectionName} x, {projectionName} y) => ");

Expand Down
7 changes: 4 additions & 3 deletions src/WinRT.Projection.Writer/Factories/AbiStructFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ internal static class AbiStructFactory
public static void WriteAbiStruct(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type)
{
// Emit the underlying ABI struct only when not blittable AND not a mapped struct
// (mapped structs like Duration/KeyTime/RepeatBehavior have addition files that
// replace the public struct's field layout, so a per-field ABI struct can't be
// built directly from the projected type).
// (mapped structs like Duration/KeyTime/RepeatBehavior are defined in full by an
// addition file rather than generated from metadata, so the writer can't build a
// per-field ABI struct from the projected type; those types instead guarantee a
// layout-compatible shape themselves and are passed through by value).
bool blittable = AbiTypeHelpers.IsTypeBlittable(context.Cache, type);
(string typeNs, string typeNm) = type.Names();
bool isMappedStruct = MappedTypes.Get(typeNs, typeNm) is not null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ internal static void WriteStructEnumMarshallerClass(IndentedTextWriter writer, P

// For structs that are mapped (e.g. Duration, KeyTime, RepeatBehavior — they have
// EmitAbi=true and an addition file that completely replaces the public struct), skip
// the per-field ConvertToUnmanaged/ConvertToManaged because the projected struct's
// public fields don't match the WinMD field layout. The truth marshaller for these
// contains only BoxToUnmanaged/UnboxToManaged.
// the per-field ConvertToUnmanaged/ConvertToManaged: no ABI struct is emitted for them
// (see AbiStructFactory), as they are passed through by value. The truth marshaller for
// these contains only BoxToUnmanaged/UnboxToManaged.
(string typeNs, string typeNm) = type.Names();
bool isMappedStruct = isNonBlittableStruct && MappedTypes.Get(typeNs, typeNm) is not null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ namespace Microsoft.UI.Xaml.Media.Animation
[WindowsRuntimeClassName("Windows.Foundation.IReference`1<Microsoft.UI.Xaml.Media.Animation.KeyTime>")]
[ABI.Microsoft.UI.Xaml.Media.Animation.KeyTimeComWrappersMarshaller]
#endif
public readonly struct KeyTime : IEquatable<KeyTime>
public struct KeyTime : IEquatable<KeyTime>
{
public TimeSpan TimeSpan;

public static KeyTime FromTimeSpan(TimeSpan timeSpan)
{
ArgumentOutOfRangeException.ThrowIfLessThan(timeSpan, TimeSpan.Zero, nameof(timeSpan));
Expand Down Expand Up @@ -56,10 +58,5 @@ public static implicit operator KeyTime(TimeSpan timeSpan)
{
return KeyTime.FromTimeSpan(timeSpan);
}

public TimeSpan TimeSpan
{
readonly get; private init;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ namespace Microsoft.UI.Xaml.Media.Animation
#endif
public struct RepeatBehavior : IFormattable, IEquatable<RepeatBehavior>
{
public double Count;
public TimeSpan Duration;
public RepeatBehaviorType Type;

internal static bool IsFinite(double value)
{
return !(double.IsNaN(value) || double.IsInfinity(value));
Expand Down Expand Up @@ -63,21 +67,6 @@ public readonly bool HasDuration
}
}

public double Count
{
readonly get; set;
}

public TimeSpan Duration
{
readonly get; set;
}

public RepeatBehaviorType Type
{
readonly get; set;
}

public readonly override string ToString()
{
return InternalToString(null, null);
Expand Down
Loading