diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md
index 8acb415788..e1c6e605f8 100644
--- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md
+++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md
@@ -23,4 +23,7 @@ CSWINRT2013 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersio
CSWINRT2014 | WindowsRuntime.SourceGenerator | Warning | API contract type missing 'ContractVersionAttribute'
CSWINRT2015 | WindowsRuntime.SourceGenerator | Info | Public authored type missing version metadata
CSWINRT2016 | WindowsRuntime.SourceGenerator | Warning | Public authored type missing 'ContractVersionAttribute'
-CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]'
\ No newline at end of file
+CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]'
+CSWINRT2018 | WindowsRuntime.SourceGenerator | Warning | '[WindowsRuntimeNativeExposedType]' target type cannot be instantiated
+CSWINRT2019 | WindowsRuntime.SourceGenerator | Warning | '[WindowsRuntimeNativeExposedType]' target type is not a projected class
+CSWINRT2020 | WindowsRuntime.SourceGenerator | Warning | Duplicate '[WindowsRuntimeNativeExposedType]' target type
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs
new file mode 100644
index 0000000000..4f7ff205ee
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace WindowsRuntime.SourceGenerator.Diagnostics;
+
+///
+/// A diagnostic analyzer that validates uses of [WindowsRuntimeNativeExposedType]. The attribute is only
+/// meaningful when applied with a concrete, non generic projected Windows Runtime class type, as CCW marshalling
+/// code is generated automatically for every other kind of type. It also reports redundant applications of the
+/// attribute that target a type already used by another application in the same assembly.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class WindowsRuntimeNativeExposedTypeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } =
+ [
+ DiagnosticDescriptors.NativeExposedTypeNotInstantiable,
+ DiagnosticDescriptors.NativeExposedTypeNotProjectedClass,
+ DiagnosticDescriptors.NativeExposedTypeDuplicate
+ ];
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationAction(static context =>
+ {
+ // Get the '[WindowsRuntimeNativeExposedType]' symbol
+ if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.InteropServices.WindowsRuntimeNativeExposedTypeAttribute") is not { } nativeExposedTypeAttributeType)
+ {
+ return;
+ }
+
+ // Get the '[WindowsRuntimeMetadata]' symbol, which is used to detect projected Windows Runtime types
+ if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.WindowsRuntimeMetadataAttribute") is not { } windowsRuntimeMetadataAttributeType)
+ {
+ return;
+ }
+
+ List validTargetTypes = [];
+
+ // Collect the valid target types across all applications of the attribute in the assembly (including
+ // the ones in generated code), so that a type used both in user code and in generated code is still
+ // detected as a duplicate. This list is only used to report duplicate applications of the attribute.
+ foreach (AttributeData attribute in context.Compilation.Assembly.GetAttributes(nativeExposedTypeAttributeType))
+ {
+ if (attribute is { ConstructorArguments: [{ Value: ITypeSymbol targetType }] } &&
+ Classify(targetType, windowsRuntimeMetadataAttributeType) is NativeExposedTypeKind.Valid)
+ {
+ validTargetTypes.Add(targetType);
+ }
+ }
+
+ // Classify and report each application of the attribute. Applications in generated code are
+ // suppressed by the analysis framework, because this analyzer opts out of generated code analysis,
+ // so only applications authored in user code are ever reported.
+ foreach (AttributeData attribute in context.Compilation.Assembly.GetAttributes(nativeExposedTypeAttributeType))
+ {
+ // Skip malformed applications, eg. where the argument does not bind to a type
+ if (attribute is not { ConstructorArguments: [{ Value: ITypeSymbol targetType }] })
+ {
+ continue;
+ }
+
+ Location? location = attribute.GetArgumentLocation(0, context.CancellationToken) ?? attribute.GetLocation(context.CancellationToken);
+
+ NativeExposedTypeKind kind = Classify(targetType, windowsRuntimeMetadataAttributeType);
+
+ // The type cannot be instantiated, so no CCW would ever be created for it
+ if (kind is NativeExposedTypeKind.NotInstantiable)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.NativeExposedTypeNotInstantiable,
+ location,
+ targetType));
+ }
+ else if (kind is NativeExposedTypeKind.NotProjectedClass)
+ {
+ // The type is not a projected class, so CCW marshalling code is already generated for it
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.NativeExposedTypeNotProjectedClass,
+ location,
+ targetType));
+ }
+ else if (CountOccurrences(validTargetTypes, targetType) > 1)
+ {
+ // The type is valid, but it is also targeted by another application of the attribute
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.NativeExposedTypeDuplicate,
+ location,
+ targetType));
+ }
+ }
+ });
+ }
+
+ ///
+ /// Classifies a target type used with [WindowsRuntimeNativeExposedType].
+ ///
+ /// The target type to classify.
+ /// The [WindowsRuntimeMetadata] symbol.
+ /// The classification for .
+ private static NativeExposedTypeKind Classify(ITypeSymbol type, INamedTypeSymbol windowsRuntimeMetadataAttributeType)
+ {
+ // A type that cannot be instantiated can never have a CCW created for it, so the attribute is meaningless
+ if (!IsInstantiable(type))
+ {
+ return NativeExposedTypeKind.NotInstantiable;
+ }
+
+ // A valid target is a non generic projected Windows Runtime class. CCW marshalling code is generated
+ // automatically for every other kind of type, so the attribute is only ever needed for projected classes.
+ if (type is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsGenericType: false } namedType ||
+ !namedType.HasAttributeWithType(windowsRuntimeMetadataAttributeType))
+ {
+ return NativeExposedTypeKind.NotProjectedClass;
+ }
+
+ // The type is a valid, non-generic and projected runtime class
+ return NativeExposedTypeKind.Valid;
+ }
+
+ ///
+ /// Counts how many times a given type appears in a sequence of types.
+ ///
+ /// The sequence of types to search.
+ /// The type to count occurrences of.
+ /// The number of times appears in .
+ private static int CountOccurrences(IEnumerable types, ITypeSymbol type)
+ {
+ int count = 0;
+
+ foreach (ITypeSymbol candidate in types)
+ {
+ if (SymbolEqualityComparer.Default.Equals(candidate, type))
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// Checks whether a given type can be instantiated.
+ ///
+ /// The type to check.
+ /// Whether can be instantiated.
+ private static bool IsInstantiable(ITypeSymbol type)
+ {
+ // Array types can always be instantiated
+ if (type is IArrayTypeSymbol)
+ {
+ return true;
+ }
+
+ // Any other non named type (eg. a pointer type or a type parameter) cannot be instantiated
+ if (type is not INamedTypeSymbol namedType)
+ {
+ return false;
+ }
+
+ // An unbound generic type definition (eg. 'typeof(List<>)') cannot be instantiated
+ if (namedType.IsUnboundGenericType)
+ {
+ return false;
+ }
+
+ // Interfaces, abstract classes, and static classes cannot be instantiated
+ if (namedType.TypeKind is TypeKind.Interface || namedType.IsAbstract || namedType.IsStatic)
+ {
+ return false;
+ }
+
+ // Classes, structs, enums, and delegates can be instantiated
+ return namedType.TypeKind is TypeKind.Class or TypeKind.Struct or TypeKind.Enum or TypeKind.Delegate;
+ }
+
+ ///
+ /// The classification of a target type used with [WindowsRuntimeNativeExposedType].
+ ///
+ private enum NativeExposedTypeKind
+ {
+ ///
+ /// The target type cannot be instantiated (eg. an interface, an abstract class, or a generic type definition).
+ ///
+ NotInstantiable,
+
+ ///
+ /// The target type can be instantiated, but it is not a projected Windows Runtime class.
+ ///
+ NotProjectedClass,
+
+ ///
+ /// The target type is a valid, non generic projected Windows Runtime class.
+ ///
+ Valid
+ }
+}
diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs
index 65b19e356e..84567ce6ce 100644
--- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs
@@ -244,4 +244,46 @@ internal static partial class DiagnosticDescriptors
isEnabledByDefault: true,
description: "Public types in a Windows Runtime component should not mix '[ContractVersion]' and '[Version]', as these represent two different versioning schemes. Pick one and apply it consistently across the public API surface of the component.",
helpLinkUri: "https://github.com/microsoft/CsWinRT");
+
+ ///
+ /// Gets a for a [WindowsRuntimeNativeExposedType] attribute applied with a target type that cannot be instantiated.
+ ///
+ public static readonly DiagnosticDescriptor NativeExposedTypeNotInstantiable = new(
+ id: "CSWINRT2018",
+ title: "'[WindowsRuntimeNativeExposedType]' target type cannot be instantiated",
+ messageFormat: """The type '{0}' is used as the target of a '[WindowsRuntimeNativeExposedType]' attribute, but it cannot be instantiated. Only concrete, non generic projected Windows Runtime class types can be used with this attribute, as no CCW would ever be created for a type that cannot be instantiated.""",
+ category: "WindowsRuntime.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "The '[WindowsRuntimeNativeExposedType]' attribute should only be applied with concrete, non generic projected Windows Runtime class types. Types that cannot be instantiated, such as interfaces, abstract classes, static classes, and generic type definitions, have no effect when used as the target of the attribute.",
+ helpLinkUri: "https://github.com/microsoft/CsWinRT",
+ customTags: WellKnownDiagnosticTags.CompilationEnd);
+
+ ///
+ /// Gets a for a [WindowsRuntimeNativeExposedType] attribute applied with a target type that is not a projected Windows Runtime class.
+ ///
+ public static readonly DiagnosticDescriptor NativeExposedTypeNotProjectedClass = new(
+ id: "CSWINRT2019",
+ title: "'[WindowsRuntimeNativeExposedType]' target type is not a projected class",
+ messageFormat: """The type '{0}' is used as the target of a '[WindowsRuntimeNativeExposedType]' attribute, but it is not a projected Windows Runtime class type. CCW marshalling code is already generated automatically for all other types (such as user defined types, value types, delegates, arrays, and generic instantiations), so applying the attribute to '{0}' has no effect.""",
+ category: "WindowsRuntime.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "The '[WindowsRuntimeNativeExposedType]' attribute should only be applied with projected Windows Runtime class types. CCW marshalling code is generated automatically for all other types, so applying the attribute to them has no effect.",
+ helpLinkUri: "https://github.com/microsoft/CsWinRT",
+ customTags: WellKnownDiagnosticTags.CompilationEnd);
+
+ ///
+ /// Gets a for a [WindowsRuntimeNativeExposedType] attribute applied with a target type that is already used by another application of the attribute in the same assembly.
+ ///
+ public static readonly DiagnosticDescriptor NativeExposedTypeDuplicate = new(
+ id: "CSWINRT2020",
+ title: "Duplicate '[WindowsRuntimeNativeExposedType]' target type",
+ messageFormat: """The type '{0}' is used as the target of more than one '[WindowsRuntimeNativeExposedType]' attribute in the same assembly. CCW marshalling code is only generated once per type, so the additional applications of the attribute for '{0}' have no effect and can be removed.""",
+ category: "WindowsRuntime.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "A given type should only be used as the target of a single '[WindowsRuntimeNativeExposedType]' attribute in an assembly. CCW marshalling code is only generated once per type, so additional applications of the attribute for the same type have no effect.",
+ helpLinkUri: "https://github.com/microsoft/CsWinRT",
+ customTags: WellKnownDiagnosticTags.CompilationEnd);
}
\ No newline at end of file
diff --git a/src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj b/src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj
new file mode 100644
index 0000000000..d588e3b38a
--- /dev/null
+++ b/src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ $(FunctionalTestsBuildTFMs)
+ x86;x64
+ win-x86;win-x64
+ $(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml
+ true
+
+
+
+
+
+
+
+
diff --git a/src/Tests/FunctionalTests/NativeExposedType/Program.cs b/src/Tests/FunctionalTests/NativeExposedType/Program.cs
new file mode 100644
index 0000000000..538d3f1d92
--- /dev/null
+++ b/src/Tests/FunctionalTests/NativeExposedType/Program.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using Windows.Data.Json;
+using WindowsRuntime.InteropServices;
+
+#pragma warning disable CSWINRT3001 // Type or member is obsolete
+
+// The interop generator normally skips projected types when generating CCW marshalling code, as they are backed
+// by native objects and never need to be exposed to native code through a CCW. Opting 'JsonArray' in via the
+// '[WindowsRuntimeNativeExposedType]' attribute forces the interop generator to also emit CCW marshalling code
+// for it, just like it does for any user defined type. This registers a proxy type map association for the type,
+// which the checks below then verify is present (and absent for a projected type that was not opted in).
+[assembly: WindowsRuntimeNativeExposedType(typeof(JsonArray))]
+
+namespace NativeExposedType;
+
+internal static class Program
+{
+ private static int Main()
+ {
+ IReadOnlyDictionary proxyTypeMapping = GetComWrappersProxyTypeMapping();
+
+ // 'JsonArray' was explicitly opted into CCW marshalling code generation, so the interop generator must
+ // have generated a proxy for it and registered the associated proxy type map entry.
+ if (!proxyTypeMapping.TryGetValue(typeof(JsonArray), out _))
+ {
+ return 101;
+ }
+
+ // 'JsonObject' is a projected type that was not opted into CCW marshalling code generation. The interop
+ // generator must have skipped it, as projected types are backed by native objects and never need CCW
+ // marshalling code generated for them by default.
+ if (proxyTypeMapping.TryGetValue(typeof(JsonObject), out _))
+ {
+ return 102;
+ }
+
+ return 100;
+ }
+
+ // Retrieves the proxy type map used by CsWinRT to resolve CCW marshalling info for managed objects
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The proxy type map is always preserved by the interop generator.")]
+ private static IReadOnlyDictionary GetComWrappersProxyTypeMapping()
+ {
+ return TypeMapping.GetOrCreateProxyTypeMapping();
+ }
+}
diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs
index ad67904e34..8345282892 100644
--- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs
+++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs
@@ -60,12 +60,14 @@ protected override ParseOptions CreateParseOptions()
/// Whether to enable unsafe blocks.
/// The language version to use to run the test.
/// Whether to set the "CsWinRTComponent" MSBuild property to .
+ /// An additional source file to add to the compilation as generated code, or to not add one.
public static Task VerifyAnalyzerAsync(
string source,
ReadOnlySpan expectedDiagnostics = default,
bool allowUnsafeBlocks = true,
LanguageVersion languageVersion = LanguageVersion.CSharp14,
- bool isCsWinRTComponent = false)
+ bool isCsWinRTComponent = false,
+ string generatedSource = null)
{
CSharpAnalyzerTest test = new(allowUnsafeBlocks, languageVersion) { TestCode = source };
@@ -75,6 +77,14 @@ public static Task VerifyAnalyzerAsync(
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location));
test.TestState.ExpectedDiagnostics.AddRange([.. expectedDiagnostics]);
+ // Add an additional source file that is treated as generated code (the '.g.cs' suffix is recognized by the
+ // analysis framework). This is used to validate that diagnostics are suppressed for applications of the
+ // attribute that appear in generated code, when the analyzer opts out of generated code analysis.
+ if (generatedSource is not null)
+ {
+ test.TestState.Sources.Add(("NativeExposedTypes.g.cs", generatedSource));
+ }
+
// Configure the desired MSBuild properties via a global analyzer config file
if (isCsWinRTComponent)
{
diff --git a/src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs
new file mode 100644
index 0000000000..85dd530542
--- /dev/null
+++ b/src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs
@@ -0,0 +1,229 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+using WindowsRuntime.SourceGenerator.Diagnostics;
+using WindowsRuntime.SourceGenerator.Tests.Helpers;
+
+namespace WindowsRuntime.SourceGenerator.Tests;
+
+using VerifyCS = CSharpAnalyzerTest;
+
+///
+/// Tests for .
+///
+[TestClass]
+public sealed class Test_WindowsRuntimeNativeExposedTypeAnalyzer
+{
+ [TestMethod]
+ public async Task ProjectedClass_DoesNotWarn()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType(typeof(Microsoft.UI.Xaml.DependencyObjectCollection))]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task Interface_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(IMyInterface)|})]
+
+ public interface IMyInterface;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task AbstractClass_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(MyClass)|})]
+
+ public abstract class MyClass;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task StaticClass_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(MyClass)|})]
+
+ public static class MyClass;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task GenericTypeDefinition_Warns()
+ {
+ const string source = """
+ using System.Collections.Generic;
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(List<>)|})]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task UserDefinedClass_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyClass)|})]
+
+ public sealed class MyClass;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ValueType_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyStruct)|})]
+
+ public struct MyStruct;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task Delegate_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyDelegate)|})]
+
+ public delegate void MyDelegate();
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ArrayType_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(int[])|})]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ClosedGenericType_Warns()
+ {
+ const string source = """
+ using System.Collections.Generic;
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(List)|})]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task ProjectedStruct_Warns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(Windows.Foundation.Point)|})]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task DuplicateProjectedClass_BothWarn()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2020:typeof(Microsoft.UI.Xaml.DependencyObjectCollection)|})]
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2020:typeof(Microsoft.UI.Xaml.DependencyObjectCollection)|})]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task DuplicateNonProjectedClass_WarnsAsNotProjectedClass()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyClass)|})]
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyClass)|})]
+
+ public sealed class MyClass;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ public async Task DuplicateAcrossUserAndGeneratedCode_OnlyUserWarns()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2020:typeof(Microsoft.UI.Xaml.DependencyObjectCollection)|})]
+ """;
+
+ const string generatedSource = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType(typeof(Microsoft.UI.Xaml.DependencyObjectCollection))]
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source, generatedSource: generatedSource);
+ }
+
+ [TestMethod]
+ public async Task ApplicationInGeneratedCodeOnly_DoesNotWarn()
+ {
+ const string source = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType(typeof(Microsoft.UI.Xaml.DependencyObjectCollection))]
+ """;
+
+ const string generatedSource = """
+ using WindowsRuntime.InteropServices;
+
+ [assembly: WindowsRuntimeNativeExposedType(typeof(MyClass))]
+
+ public sealed class MyClass;
+ """;
+
+ await VerifyCS.VerifyAnalyzerAsync(source, generatedSource: generatedSource);
+ }
+}
diff --git a/src/Tests/UnitTest/NativeExposedTypeTests.cs b/src/Tests/UnitTest/NativeExposedTypeTests.cs
new file mode 100644
index 0000000000..34e3c1fac8
--- /dev/null
+++ b/src/Tests/UnitTest/NativeExposedTypeTests.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using TestComponentCSharp;
+using Windows.Data.Json;
+using WindowsRuntime.InteropServices;
+
+#pragma warning disable CSWINRT3001 // Type or member is obsolete
+
+// The interop generator normally skips projected types when generating CCW marshalling code, as they are backed
+// by native objects and never need to be exposed to native code through a CCW. Opting 'CustomIterableTest' in via
+// the '[WindowsRuntimeNativeExposedType]' attribute forces the interop generator to also emit CCW marshalling code
+// for it, just like it does for any user defined type. This registers a proxy type map association for the type,
+// which the tests below then verify is present (and absent for a projected type that was not opted in).
+[assembly: WindowsRuntimeNativeExposedType(typeof(CustomIterableTest))]
+
+namespace UnitTest
+{
+ [TestClass]
+ public class NativeExposedTypeTests
+ {
+ [TestMethod]
+ public void NativeExposedType_IsRegisteredInComWrappersProxyTypeMapping()
+ {
+ IReadOnlyDictionary proxyTypeMapping = GetComWrappersProxyTypeMapping();
+
+ // 'CustomIterableTest' was explicitly opted into CCW marshalling code generation, so the interop
+ // generator must have generated a proxy for it and registered the associated proxy type map entry.
+ Assert.IsTrue(proxyTypeMapping.TryGetValue(typeof(CustomIterableTest), out _));
+ }
+
+ [TestMethod]
+ public void ProjectedType_WithoutOptIn_IsNotRegisteredInComWrappersProxyTypeMapping()
+ {
+ IReadOnlyDictionary proxyTypeMapping = GetComWrappersProxyTypeMapping();
+
+ // 'JsonObject' is a projected type that was not opted into CCW marshalling code generation. The interop
+ // generator must have skipped it, as projected types are backed by native objects and never need CCW
+ // marshalling code generated for them by default.
+ Assert.IsFalse(proxyTypeMapping.TryGetValue(typeof(JsonObject), out _));
+ }
+
+ // Retrieves the proxy type map used by CsWinRT to resolve CCW marshalling info for managed objects
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The proxy type map is always preserved by the interop generator.")]
+ private static IReadOnlyDictionary GetComWrappersProxyTypeMapping()
+ {
+ return TypeMapping.GetOrCreateProxyTypeMapping();
+ }
+ }
+}
diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs
index c044ab3e13..2595eb1256 100644
--- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs
+++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs
@@ -80,6 +80,11 @@ public static void TryTrackTypeHierarchyType(
/// The instance to use.
/// The instance to use.
/// The module currently being analyzed.
+ ///
+ /// Whether the type was explicitly opted into CCW marshalling code generation via
+ /// [WindowsRuntimeNativeExposedType]. When this is , projected Windows
+ /// Runtime types are not skipped, as they would otherwise never get CCW marshalling code generated.
+ ///
///
/// This method expects to either be non-generic, or
/// to have be a fully constructed signature for it.
@@ -91,7 +96,8 @@ public static void TryTrackExposedUserDefinedType(
InteropGeneratorDiscoveryState discoveryState,
InteropDefinitions interopDefinitions,
InteropReferences interopReferences,
- ModuleDefinition module)
+ ModuleDefinition module,
+ bool isNativeExposedType = false)
{
// Ignore types that should explicitly be excluded
if (TypeExclusions.IsExcluded(typeSignature, interopReferences))
@@ -112,8 +118,10 @@ public static void TryTrackExposedUserDefinedType(
return;
}
- // We can skip all projected Windows Runtime types early, as they don't need CCW support
- if (typeDefinition.IsProjectedWindowsRuntimeType)
+ // We can skip all projected Windows Runtime types early, as they don't need CCW support. The only
+ // exception is when a type has been explicitly opted into CCW marshalling code generation, in which
+ // case we do want to process it as any other user-defined type (see the analyzer for more details).
+ if (typeDefinition.IsProjectedWindowsRuntimeType && !isNativeExposedType)
{
return;
}
diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs
index 6df46e720a..0c215a73b1 100644
--- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs
+++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs
@@ -848,6 +848,22 @@ public static WellKnownInteropException DiscoverActivationFactoryTypesError(stri
return Exception(97, $"Failed to discover activation factory types for module '{name}'.", exception);
}
+ ///
+ /// Failed to discover native exposed types.
+ ///
+ public static WellKnownInteropException DiscoverNativeExposedTypesError(string? name, Exception exception)
+ {
+ return Exception(98, $"Failed to discover native exposed types (from '[WindowsRuntimeNativeExposedType]' attributes) for module '{name}'.", exception);
+ }
+
+ ///
+ /// Failed to resolve a type specified in a '[WindowsRuntimeNativeExposedType]' attribute.
+ ///
+ public static WellKnownInteropWarning NativeExposedTypeNotResolvedWarning(TypeSignature typeSignature, ModuleDefinition module)
+ {
+ return Warning(99, $"Failed to resolve the type '{typeSignature}' specified in a '[WindowsRuntimeNativeExposedType]' attribute while processing module '{module}': CCW marshalling code for it will not be generated.");
+ }
+
///
/// Creates a new exception with the specified id and message.
///
diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs
index 42760ba3c2..8d7db67314 100644
--- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs
+++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs
@@ -270,6 +270,11 @@ private static void LoadAndProcessModule(
args.Token.ThrowIfCancellationRequested();
+ // Discover all types explicitly opted into CCW marshalling code generation
+ DiscoverNativeExposedTypes(args, discoveryState, module);
+
+ args.Token.ThrowIfCancellationRequested();
+
// Discover all generic type instantiations
DiscoverGenericTypeInstantiations(args, discoveryState, module);
@@ -367,6 +372,79 @@ private static void DiscoverExposedUserDefinedTypes(
}
}
+ ///
+ /// Discovers all types explicitly opted into CCW marshalling code generation via [WindowsRuntimeNativeExposedType].
+ ///
+ /// The arguments for this invocation.
+ /// The discovery state for this invocation.
+ /// The module currently being analyzed.
+ ///
+ /// Projected Windows Runtime types are normally skipped when discovering exposed user-defined types, as they are backed
+ /// by native objects and never need CCW marshalling code. This method handles the niche cases where a projected type
+ /// does need such code, by processing the types explicitly requested through the attribute as any other user-defined type.
+ ///
+ private static void DiscoverNativeExposedTypes(
+ InteropGeneratorArgs args,
+ InteropGeneratorDiscoveryState discoveryState,
+ ModuleDefinition module)
+ {
+ try
+ {
+ // Optimization: only implementation assemblies can carry the attribute, as Windows Runtime
+ // reference assemblies only contain projections and no user authored assembly metadata.
+ if (module.Assembly is not { IsWindowsRuntimeReferenceAssembly: false } assembly)
+ {
+ return;
+ }
+
+ InteropReferences interopReferences = CreateDiscoveryInteropReferences(module);
+ InteropDefinitions interopDefinitions = new(
+ interopReferences: interopReferences,
+ windowsRuntimeSdkProjectionModule: discoveryState.WindowsRuntimeSdkProjectionModule!,
+ windowsRuntimeSdkXamlProjectionModule: discoveryState.WindowsRuntimeSdkXamlProjectionModule,
+ windowsRuntimeProjectionModule: discoveryState.WindowsRuntimeProjectionModule,
+ windowsRuntimeComponentModule: discoveryState.WindowsRuntimeComponentModule);
+
+ // Enumerate all '[assembly: WindowsRuntimeNativeExposedType(typeof())]' attributes on the module
+ foreach (CustomAttribute attribute in assembly.FindCustomAttributes("WindowsRuntime.InteropServices"u8, "WindowsRuntimeNativeExposedTypeAttribute"u8))
+ {
+ args.Token.ThrowIfCancellationRequested();
+
+ // Match '[WindowsRuntimeNativeExposedType(typeof())]' and extract the exposed type
+ if (attribute.Signature is not { FixedArguments: [{ Element: TypeSignature exposedType }] })
+ {
+ continue;
+ }
+
+ // Resolve the exposed type to its definition. If we can't, it likely means a reference is
+ // missing. We just warn and move on, as the rest of the discovery can still be completed.
+ if (!exposedType.TryResolve(interopReferences.RuntimeContext, out TypeDefinition? typeDefinition))
+ {
+ WellKnownInteropExceptions.NativeExposedTypeNotResolvedWarning(exposedType, module).LogOrThrow(args.TreatWarningsAsErrors);
+
+ continue;
+ }
+
+ // Track the exposed type as a user-defined type, bypassing the check that would normally skip
+ // projected types. All other filters still apply, so if the type is not actually a projected
+ // type that would benefit from CCW support (e.g. it's an interface), this will just be a no-op.
+ InteropTypeDiscovery.TryTrackExposedUserDefinedType(
+ typeDefinition: typeDefinition,
+ typeSignature: typeDefinition.ToTypeSignature(),
+ args: args,
+ discoveryState: discoveryState,
+ interopDefinitions: interopDefinitions,
+ interopReferences: interopReferences,
+ module: module,
+ isNativeExposedType: true);
+ }
+ }
+ catch (Exception e)
+ {
+ WellKnownInteropExceptions.DiscoverNativeExposedTypesError(module.Name, e).ThrowOrAttach(e);
+ }
+ }
+
///
/// Discovers all generic type instantiations in a given assembly.
///
diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs
index 77c1e2ed9d..045b8e5556 100644
--- a/src/WinRT.Interop.Generator/References/InteropReferences.cs
+++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs
@@ -745,6 +745,11 @@ public InteropReferences(
///
public TypeReference WindowsRuntimeMappedTypeAttribute => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "WindowsRuntimeMappedTypeAttribute"u8);
+ ///
+ /// Gets the for WindowsRuntime.InteropServices.WindowsRuntimeNativeExposedTypeAttribute.
+ ///
+ public TypeReference WindowsRuntimeNativeExposedTypeAttribute => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "WindowsRuntimeNativeExposedTypeAttribute"u8);
+
///
/// Gets the for WindowsRuntime.InteropServices.WindowsRuntimeComWrappersTypeMapGroup.
///
diff --git a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs
index fafda0e0fb..4e86a0a3d7 100644
--- a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs
+++ b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs
@@ -90,6 +90,8 @@ _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.I
=> "Windows_Foundation_IAsyncAction",
_ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IVectorChangedEventArgs)
=> "Windows_Foundation_Collections_IVectorChangedEventArgs",
+ _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IStringable)
+ => "IStringable",
_ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IBuffer)
=> "Windows_Storage_Streams_IBuffer",
_ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IInputStream)
diff --git a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs
index 77a19c1874..ec9e2ad6ea 100644
--- a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs
+++ b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs
@@ -74,8 +74,8 @@ public abstract unsafe class WindowsRuntimeComWrappersMarshallerAttribute : Attr
public virtual void* GetOrCreateComInterfaceForObject(object value)
{
[MethodImpl(MethodImplOptions.NoInlining)]
- static NotSupportedException GetNotSupportedException()
- => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation does not support '{nameof(GetOrCreateComInterfaceForObject)}'.");
+ NotSupportedException GetNotSupportedException()
+ => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation ('{GetType()}') does not support '{nameof(GetOrCreateComInterfaceForObject)}'.");
throw GetNotSupportedException();
}
@@ -108,11 +108,12 @@ static NotSupportedException GetNotSupportedException()
///
public virtual ComWrappers.ComInterfaceEntry* ComputeVtables(out int count)
{
- [MethodImpl(MethodImplOptions.NoInlining)]
- static NotSupportedException GetNotSupportedException()
- => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation does not support '{nameof(ComputeVtables)}'.");
+ // This method does not throw an exception like the others in this type. In case of failure,
+ // the code in 'WindowsRuntimeMarshallingInfo' will detect the returned 'null' value and
+ // throw an exception from there, so additional debugging info can be included as well.
+ count = 0;
- throw GetNotSupportedException();
+ return null;
}
///
@@ -134,8 +135,8 @@ static NotSupportedException GetNotSupportedException()
public virtual object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags)
{
[MethodImpl(MethodImplOptions.NoInlining)]
- static NotSupportedException GetNotSupportedException()
- => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation does not support '{nameof(CreateObject)}'.");
+ NotSupportedException GetNotSupportedException()
+ => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation ('{GetType()}') does not support '{nameof(CreateObject)}'.");
throw GetNotSupportedException();
}
diff --git a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs
new file mode 100644
index 0000000000..8f5cf79230
--- /dev/null
+++ b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace WindowsRuntime.InteropServices;
+
+///
+/// Indicates a projected Windows Runtime class type for which CCW (COM Callable Wrapper) marshalling code
+/// should be generated, so that its instances can be marshalled to native code through a CCW.
+///
+///
+///
+/// CCW marshalling code is normally generated automatically for all managed types that implement one or more
+/// Windows Runtime interfaces. Projected Windows Runtime types are an exception, as they are backed by native
+/// objects and are marshalled by unwrapping the underlying native object directly, which means they normally
+/// never require a CCW. This attribute allows explicitly opting a projected class type into CCW marshalling
+/// code generation, for the niche scenarios where such support is required.
+///
+///
+/// One such scenario involves using a projected collection type as an items source for a XAML control.
+/// Consider using a DependencyObjectCollection as the items source for an ItemsControl. The
+/// control will query the object assigned to its items source (which is marshalled as an IInspectable
+/// value) for the IBindableIterable and IIterable<IInspectable> interfaces. This normally
+/// succeeds when assigning most collection types. However, DependencyObjectCollection implements neither
+/// of those interfaces. It implements IIterable<DependencyObject>, and Windows Runtime generic
+/// interfaces are not covariant on the native side, meaning each generic instantiation of a given generic
+/// Windows Runtime interface has a completely different IID.
+///
+///
+/// To handle this, XAML controls perform the following steps:
+///
+/// - They query the source object for IBindableIterable and IIterable<IInspectable>.
+/// - If that fails, they obtain a CCW from the source object and query that CCW instead.
+///
+///
+///
+/// This might seem surprising, given that a projected type is backed by a native object, and so it would just
+/// be unwrapped when marshalled, with no CCW involved. The .NET runtime has dedicated logic for this case,
+/// where it will perform the following steps:
+///
+/// - It creates an RCW (Runtime Callable Wrapper) for the native object.
+/// - It uses ComWrappers to obtain a CCW for that RCW, to be used as a proxy.
+///
+/// That is, in this scenario the reference from the control to the object is transformed from a direct
+/// reference into a reference to a CCW that wraps an RCW over the original native object.
+///
+///
+/// Because the RCW implements IEnumerable<DependencyObject>, and that interface is covariant in
+/// C#, marshalling it to native makes the query for IIterable<IInspectable> succeed, by going
+/// through this reference cycle with the proxy object. This works because CsWinRT computes all variant versions
+/// of each implemented interface on managed objects, and generates vtables for all of them. As a consequence,
+/// such native objects can only be used as items sources from .NET applications, and not from C++ applications.
+///
+///
+/// Applying this attribute to a projected class type explicitly requests that a CCW vtable be generated for it,
+/// which would not normally be present, since the type is just a projection.
+///
+///
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
+public sealed class WindowsRuntimeNativeExposedTypeAttribute : Attribute
+{
+ ///
+ /// Creates a new instance with the specified parameters.
+ ///
+ /// The projected Windows Runtime class type to generate CCW marshalling code for.
+ public WindowsRuntimeNativeExposedTypeAttribute(Type type)
+ {
+ Type = type;
+ }
+
+ ///
+ /// Gets the projected Windows Runtime class type to generate CCW marshalling code for.
+ ///
+ public Type Type { get; }
+}
diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs
index 06c31b568f..391b17dce3 100644
--- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs
+++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs
@@ -590,6 +590,28 @@ unsafe WindowsRuntimeVtableInfo InitializeVtableInfo()
// Delegate to the vtable provider to produce the first vtable entries
ComWrappers.ComInterfaceEntry* vtableEntries = comWrappersMarshaller.ComputeVtables(out int count);
+ // When the attribute instance did not override 'ComputeVtables', it will just return 'null',
+ // which we can detect here. The reason for this is that we want to include the type of the
+ // object we failed to marshal in the exception message, but that wouldn't be available from
+ // the 'ComputeVtables' method itself. Returning 'null' is just an efficient way to signal the
+ // failure, rather than having that method throw and then catch and re-throw from here.
+ if (vtableEntries is null)
+ {
+ // Throws an exception with a message that includes the type of the object we failed to marshal
+ [DoesNotReturn]
+ [StackTraceHidden]
+ void ThrowNotSupportedException(WindowsRuntimeComWrappersMarshallerAttribute comWrappersMarshaller)
+ {
+ throw new NotSupportedException(
+ $"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation ('{comWrappersMarshaller.GetType()}') associated with the " +
+ $"metadata provider type '{_metadataProviderType}' does not support '{nameof(WindowsRuntimeComWrappersMarshallerAttribute.ComputeVtables)}'. " +
+ $"If marshalling instances of this type is required, consider using '{nameof(WindowsRuntimeNativeExposedTypeAttribute)}' to explicitly enable generating " +
+ $"marshalling code for this scenario. Additionally, please file an issue at https://github.com/microsoft/CsWinRT.");
+ }
+
+ ThrowNotSupportedException(comWrappersMarshaller);
+ }
+
return _vtableInfo ??= new(vtableEntries, count);
}
diff --git a/src/build.cmd b/src/build.cmd
index 51f471e172..e3fd0fbee0 100644
--- a/src/build.cmd
+++ b/src/build.cmd
@@ -68,7 +68,7 @@ if "%cswinrt_assembly_version%"=="" set cswinrt_assembly_version=0.0.0.0
if "%cswinrt_baseline_breaking_compat_errors%"=="" set cswinrt_baseline_breaking_compat_errors=false
if "%cswinrt_baseline_assembly_version_compat_errors%"=="" set cswinrt_baseline_assembly_version_compat_errors=false
-set cswinrt_functional_tests=JsonValueFunctionCalls, ClassActivation, Structs, Events, DynamicInterfaceCasting, Collections, Async, DerivedClassActivation, DerivedClassAsBaseClass, CCW
+set cswinrt_functional_tests=JsonValueFunctionCalls, ClassActivation, Structs, Events, DynamicInterfaceCasting, Collections, Async, DerivedClassActivation, DerivedClassAsBaseClass, CCW, NativeExposedType
if "%cswinrt_platform%" EQU "x86" set run_functional_tests=true
if "%cswinrt_platform%" EQU "x64" set run_functional_tests=true
diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx
index 27e04f3c4e..6bb694e6e2 100644
--- a/src/cswinrt.slnx
+++ b/src/cswinrt.slnx
@@ -400,6 +400,14 @@
+
+
+
+
+
+
+
+