Skip to content
Merged
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
9 changes: 5 additions & 4 deletions docs/winrt-interop-dll-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The **mangled namespace** for a given type is defined as follows:

1. **No namespace**: if the input type has no namespace, the generated namespace defaults to `ABI`.
2. **Existing namespace**: if the input type has a namespace, the generated namespace prepends `ABI.` to the original namespace.
3. **Nested types**: if the input type is nested, the namespace is derived from the outermost declaring type (i.e. the top-level enclosing type), using the same rules above.

The **mangled type name** for a given type is defined as follows:

Expand All @@ -45,11 +46,11 @@ These are the well-known assemblies and their compact identifiers:
- `System.Runtime`: `#corlib`
- `Microsoft.Windows.SDK.NET` or `Microsoft.Windows.UI.Xaml`: `#Windows`
- `WinRT.Runtime`: `#CsWinRT`
- `Microsoft.UI.Xaml.Projection`: `#WinUI2`
- `Microsoft.Graphics.Canvas.Interop`: `#Win2D`

Compact identifiers are prefixed with `#` to distinguish them from user-defined assembly names.

For types not belonging to any well-known assembly, the implementation also checks for a `[WindowsRuntimeMetadata]` attribute on the resolved type definition. If the attribute is present, the Windows Runtime metadata name from the attribute is used as the assembly identifier instead of the actual assembly name. This allows types carrying WinRT metadata to be identified by their canonical Windows Runtime name rather than the .NET assembly they happen to live in. If the attribute is not present, the raw assembly name is used as-is.

### Examples

**Primitive type**
Expand Down Expand Up @@ -134,6 +135,8 @@ primitiveType : 'bool'
| 'ulong'
| 'float'
| 'double'
| 'nint'
| 'nuint'
| 'string'
| 'object';

Expand All @@ -154,8 +157,6 @@ arrayType : '<' mangledTypeName '>' 'Array';
assemblyName : '#corlib'
| '#Windows'
| '#CsWinRT'
| '#WinUI2'
| '#Win2D'
| identifier;

// After substitutions, identifiers may contain '-' (from namespaces), '+' (nested type separators,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis;

namespace WindowsRuntime.SourceGenerator;

/// <inheritdoc cref="AuthoringExportTypesGenerator"/>
public partial class AuthoringExportTypesGenerator
{
/// <summary>
/// Helper methods for <see cref="AuthoringExportTypesGenerator"/>.
/// </summary>
private static class Helpers
{
/// <summary>
/// Tries to get the name of a dependent Windows Runtime component from a given assembly.
/// </summary>
/// <param name="assemblySymbol">The assembly symbol to analyze.</param>
/// <param name="compilation">The <see cref="Compilation"/> instance to use.</param>
/// <param name="token">The <see cref="CancellationToken"/> instance to use.</param>
/// <param name="name">The resulting type name, if found.</param>
/// <returns>Whether a type name was found.</returns>
public static bool TryGetDependentAssemblyExportsTypeName(
IAssemblySymbol assemblySymbol,
Compilation compilation,
CancellationToken token,
[NotNullWhen(true)] out string? name)
{
// Get the attribute to lookup to find the target type to use
INamedTypeSymbol winRTAssemblyExportsTypeAttributeSymbol = compilation.GetTypeByMetadataName("WindowsRuntime.InteropServices.WindowsRuntimeComponentAssemblyExportsTypeAttribute")!;

// Make sure the assembly does have the attribute on it
if (!assemblySymbol.TryGetAttributeWithType(winRTAssemblyExportsTypeAttributeSymbol, out AttributeData? attributeData))
{
name = null;

return false;
}

token.ThrowIfCancellationRequested();

// Sanity check: we should have a valid type in the annotation
if (attributeData.ConstructorArguments is not [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol assemblyExportsTypeSymbol }])
{
name = null;

return false;
}

token.ThrowIfCancellationRequested();

// Other sanity check: this type should be accessible from this compilation
if (!assemblyExportsTypeSymbol.IsAccessibleFromCompilationAssembly(compilation))
{
name = null;

return false;
}

token.ThrowIfCancellationRequested();

name = assemblyExportsTypeSymbol.ToDisplayString();

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis;
using WindowsRuntime.SourceGenerator.Models;

namespace WindowsRuntime.SourceGenerator;

/// <summary>
/// A generator to emit export types needing for authoring scenarios.
/// </summary>
[Generator]
public sealed partial class AuthoringExportTypesGenerator : IIncrementalGenerator
{
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Get the options for the generator from the analyzer options
IncrementalValueProvider<AuthoringExportTypesOptions> options = context.AnalyzerConfigOptionsProvider.Select(Execute.GetOptions);

// Get the generation info specific to managed exports
IncrementalValueProvider<AuthoringManagedExportsInfo> managedExportsInfo = context.CompilationProvider
.Combine(options)
.Select(Execute.GetManagedExportsInfo);

// Get the generation info specific to native exports
IncrementalValueProvider<AuthoringNativeExportsInfo> nativeExportsInfo = context.CompilationProvider
.Combine(options)
.Select(Execute.GetNativeExportsInfo);

// Generate the managed exports type
context.RegisterImplementationSourceOutput(managedExportsInfo, Execute.EmitManagedExports);

// Generate the native exports type
context.RegisterImplementationSourceOutput(nativeExportsInfo, Execute.EmitNativeExports);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,24 @@ public bool GetPublishAot()
return options.GetBooleanProperty("PublishAot");
}

/// <summary>
/// Gets the value of the <c>"CsWinRTComponent"</c> property.
/// </summary>
/// <returns>The value of the <c>"CsWinRTComponent"</c> property.</returns>
public bool GetCsWinRTComponent()
{
return options.GetBooleanProperty("CsWinRTComponent");
}

/// <summary>
/// Gets the value of the <c>"CsWinRTMergeReferencedActivationFactories"</c> property.
/// </summary>
/// <returns>The value of the <c>"CsWinRTMergeReferencedActivationFactories"</c> property.</returns>
public bool GetCsWinRTMergeReferencedActivationFactories()
{
return options.GetBooleanProperty("CsWinRTMergeReferencedActivationFactories");
}

/// <summary>
/// Tries to get the value of a boolean MSBuild property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace WindowsRuntime.SourceGenerator;
/// </summary>
internal static class ISymbolExtensions
{
/// <param name="symbol">The input <see cref="ISymbol"/> instance.</param>
extension(ISymbol symbol)
{
/// <summary>
Expand Down Expand Up @@ -47,5 +48,15 @@ public bool TryGetAttributeWithType(ITypeSymbol typeSymbol, [NotNullWhen(true)]

return false;
}

/// <summary>
/// Checks whether a given symbol is accessible from the assembly of a given compilation (including eg. through nested types).
/// </summary>
/// <param name="compilation">The <see cref="Compilation"/> instance currently in use.</param>
/// <returns>Whether <paramref name="symbol"/> is accessible from the assembly for <paramref name="compilation"/>.</returns>
public bool IsAccessibleFromCompilationAssembly(Compilation compilation)
{
return compilation.IsSymbolAccessibleWithin(symbol, compilation.Assembly);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Ported from ComputeSharp.
// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Helpers/IndentedTextWriter.cs.

using System;

namespace WindowsRuntime.SourceGenerator;

/// <summary>
/// Extension methods for the <see cref="IndentedTextWriter"/> type.
/// </summary>
internal static class IndentedTextWriterExtensions
{
/// <summary>
/// Writes the following attributes into a target writer:
/// <code>
/// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
/// [global::System.Diagnostics.DebuggerNonUserCode]
/// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
/// </code>
/// </summary>
/// <param name="writer">The <see cref="IndentedTextWriter"/> instance to write into.</param>
/// <param name="generatorName">The name of the generator.</param>
/// <param name="useFullyQualifiedTypeNames">Whether to use fully qualified type names or not.</param>
/// <param name="includeNonUserCodeAttributes">Whether to also include the attribute for non-user code.</param>
public static void WriteGeneratedAttributes(
this ref IndentedTextWriter writer,
string generatorName,
bool useFullyQualifiedTypeNames = true,
bool includeNonUserCodeAttributes = true)
{
// We can use this class to get the assembly, as all files for generators are just included
// via shared projects. As such, the assembly will be the same as the generator type itself.
Version assemblyVersion = typeof(IndentedTextWriterExtensions).Assembly.GetName().Version!;

if (useFullyQualifiedTypeNames)
{
writer.WriteLine($$"""[global::System.CodeDom.Compiler.GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]""");

if (includeNonUserCodeAttributes)
{
writer.WriteLine($$"""[global::System.Diagnostics.DebuggerNonUserCode]""");
writer.WriteLine($$"""[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]""");
}
}
else
{
writer.WriteLine($$"""[GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]""");

if (includeNonUserCodeAttributes)
{
writer.WriteLine($$"""[DebuggerNonUserCode]""");
writer.WriteLine($$"""[ExcludeFromCodeCoverage]""");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.CSharp;

#pragma warning disable IDE0046

namespace WindowsRuntime.SourceGenerator;

/// <summary>
/// Extensions for <see cref="string"/>.
/// </summary>
internal static class StringExtensions
{
/// <param name="value">The input <see cref="string"/> value.</param>
extension(string value)
{
/// <summary>
/// Escapes an identifier name.
/// </summary>
/// <returns>The escaped identifier name from the current value.</returns>
public string EscapeIdentifierName()
{
// If the current value is empty, we just use an underscore for the identifier name. This
// should generally never be the case, as this method is mostly used for assembly names.
if (value is null or "")
{
return "_";
}

string escapedValue;

// Optimization: we can just return the name as is if it's already a valid identifier. Generally speaking this
// should always be the case for assembly names (except if they have one or more parts separated by a '.').
if (SyntaxFacts.IsValidIdentifier(value))
{
escapedValue = value;
}
else
{
DefaultInterpolatedStringHandler handler = new(literalLength: value.Length, formattedCount: 0);

// Add a leading '_' if the first character is not a valid start character for an identifier
if (!SyntaxFacts.IsIdentifierStartCharacter(value[0]))
{
handler.AppendFormatted('_');
}

// Build the escaped name by just replacing all invalid characters with '_'
foreach (char c in value)
{
handler.AppendFormatted(SyntaxFacts.IsIdentifierPartCharacter(c) ? c : '_');
}

escapedValue = handler.ToStringAndClear();
}

// If the resulting identifier is still not valid (eg. a keyword like 'class'),
// adjust it so that the final result is actually a valid identifier we can use.
if (SyntaxFacts.GetKeywordKind(escapedValue) is not SyntaxKind.None)
{
return $"_{escapedValue}";
}

return escapedValue;
}
}
}
Loading