microsoft/CsWinRT

Provide a fully embedded WinRT interop option

AaronRobinsonMSFT opened this issue ยท 17 comments

There are scenarios where the developer would like to use a WinRT API without polluting/altering their dependencies. This could potentially be limited to cases where the usage of the WinRT API is entirely internal to the library.

It would be nice if the CsWinRT tool could provide a comprehensive WinRT interop scenario, that is internal to the assembly, in the form of just source code - no WinRT.Runtime.dll dependency.

/cc @jkotas @richlander @clairernovotny

How common are these scenarios? Are we referring only to app projects? Otherwise, how could a library project know whether it would ever be combined with other winrt code downstream? If a project is prepared to generate and compile the entirety of the cswinrt projection and runtime sources, could it not just as easily use il linking to eliminate the transitive winrt.runtime.dll dependency? This seems like a lot of work for a narrow speculative case.

I expect these to be the most common case and should be the defaults for all libraries. The .NET Tool chain does not normally use IL Linking as standard practice for libraries and IL Linking currently interferes with reproducible builds.

IL Linking and trimming would be done at the application level on publish.

The greater concern is that this would make all such libraries mutually incompatible. The winrt.runtime.dll exists to provide single definitions for RCW/CCW caches, projected types, etc. A similar justification exists for the Windows SDK and WinUI interop dlls produced from cswinrt - these are effectively PIAs. The Windows/WinUI projections could be generated by each app (and the main readme.md page here allows for that as a fallback), but the consequence is conflicting definitions of projected types.

PIA's can be, and are, embedded in .NET to avoid these issues. That has been the recommendation since .NET 4 https://docs.microsoft.com/en-us/dotnet/framework/interop/deploying-an-interop-application

PIAs can rely in the runtime's support for Type Equivalence to avoid the type identity problem. The type equivalence algorithm does not cover all possible cases of the WinRT type system, so C#/WinRT can't rely on it without some harsh corners where support would just drop off. @AaronRobinsonMSFT would know more about the specifics of the limitations of Type Equivalence.

Understood, and that's what we need to solve.

PIA's can be, and are, embedded in .NET to avoid these issues.

PIA's have never worked for WinRT because of generic classes. As @jkoritzinsky mentioned Type Equivalence is the underlying technology that enables PIA and that is limited to COM scenarios and some small cut outs for other scenarios. Changing that feature to support WinRT would be substantial and has been discussed before - it is unlikely to happen.

@Scottj1s What about the fully embedded scenario for cases where the WinRT use is entirely internal? That seems like a reasonable scenario to support, no?

That scenario sounds like it's covered by #78, and only at app scope, yes? I.e., how can a library author be certain that theirs is the sole winrt-based module in any consuming process? The moment it's combined with any other winrt-based lib, we have the potential for compat issues. And these would be very easy to trip over - even two libs with modest, consume-only use of winrt APIs could have issues around RCW identity, etc.

With a fix to dotnet/runtime#37492 with a corresponding fix to the WeakReference support to consume it, we should be able to come up with a design that says "if you don't use WinUI, you can have multiple isolated WinRT-based modules that work together as long as they never exchange objects" (Support for reference tracking makes the WinUI scenario significantly more difficult to safely reason about). A fix to the above issue should fix the RCW identity problem since each embedded system would be completely self-contained and neither system would have any way of knowing that there's another RCW for the same object since they never interact.

The winrt.runtime.dll exists to provide single definitions for RCW/CCW caches

What about custom winmd RCWs that are not part of Windows? Like user defined controls programmed in C++? We fully expect having to consume third party winmd files as well as providing custom projections for classic Windows COM. I hope that winrt.runtime.dll is "open enough" to live alongside third party projections, otherwise you are building a recipe for failure, there never can be a single source of truth for interop definitions.

Also, what about the "triangle dependency" problem?

  • third party C++ custom control library (A) - so can't define the RCW via this projects nuget package
  • two distinct libraries (B1) and (B2) both working with (A)
  • application (C) requiring both (B1) and (B2) to work with the same control (A)

If referencing a WinRT control requires generating an RCW outside the runtime in (B1) and (B2) you better have some strategy for different RCWs to interop

Similar problems may occur with CCWs.

WinRT.Runtime only includes projections for type mappings, so it should be able to live alongside projections of 3rd party components since the third party components would depend on WinRT.Runtime (or be generated with an embedded runtime once one exists).

For the triangle dependency problem in the non-embedded scenario: the owner of the C++ custom control library is required to produce the interop package and ship it themselves. That way there is one source of truth for the RCW definition.

For the embedded scenario, here's a quick writeup of the proposal discussed over email:

Provide a Microsoft.CsWinRT.Runtime.Embedded.Sources package that has cswinrt.exe, a copy of the WinRT.Runtime sources all marked as internal with modifications to not register the ComWrappers instance globally, and a file that includes an attribute, [assembly:IncludesEmbeddedWinRTProjection]. When the WinRT.Runtime code tries to look up a type from its WinRT name, it will first check if the assembly it is in has this attribute. If it does, it will only search this assembly and the (local copy of the) type mapping registrations for the type. If it doesn't have the attribute, it will search every loaded assembly (as is the current behavior today) except assemblies with the IncludesEmbeddedWinRTProjectionAttribute attribute. As a result, embedded WinRT projections will only know about the types that were projected with them, and the global projections will be discovered correctly by the global ComWrappers instance. An important note here is we need to match the attribute on name, not on type since each embedded projection will have its own definition of the type.

Clearing the milestone on this based on current timeline & resourcing.

With .NET Core 3.1, I was able to use reflection to detect access to WinRT APIs and light them up if I'm running on Windows. This was great because I relied on WinRT APIs available since Windows 8.0.
With .NET5, reflection to WinRT is no longer supported, and the only way to call WinRT APIs are by targeting net5-windows10.0.17763. This works great for building windows apps, but it works pretty poorly for building cross-platform .NET apps, or building class libraries. Sure I can multi-target, but I'd prefer just targeting net5, because if a user is targeting net5.0 or even net5.0-windows, it'll fallback to the netcoreapp3.1 assembly which is likely still using reflection into WinRT APIs and will fail on .NET 5. I covered some of that here: #458

My thought was to build my own WinRT Projection and embed into my assembly, and throw some try/catch and OS checks around it, so I that way would detect support for those APIs and use them. However I didn't have much luck with that.

So I made the following project to get access to GeoLocation APIs, and compiled it:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Windows.CsWinRT" Version="0.8.0" PrivateAssets="All" />
  </ItemGroup>
  <PropertyGroup>
    <CsWinRTIncludes>
      Windows.Devices.Geolocation;
      Windows.Foundation.Metadata.ContractVersion;
      Windows.Foundation.UniversalApiContract;
      Windows.Foundation.Metadata.DualApiPartition;
      Windows.Foundation.Metadata.MarshalingBehavior;
      Windows.Foundation.Metadata.MarshalingType;
      Windows.Foundation.Metadata.Activatable;
      Windows.Foundation.Metadata.Static;
      Windows.Foundation.Metadata.Threading;
      Windows.Foundation.Metadata.ThreadingModel;
      Windows.Foundation.Metadata.Muse;
      Windows.Foundation.Metadata.ExclusiveTo;
      Windows.Foundation.Metadata.ApiContract;
      Windows.Foundation.Metadata.AttributeName;
      Windows.Foundation.Metadata.AllowMultiple;
      Windows.Foundation.IAsyncOperation;
      Windows.Foundation.TypedEventHandler;
      Windows.Foundation.UniversalApiContract;
      Windows.Foundation.FoundationContract;
      Windows.Foundation.IAsyncInfo;
      Windows.Foundation.IAsyncAction;
      Windows.Foundation.AsyncStatus;
      Windows.Foundation.AsyncActionCompletedHandler;
      Windows.Foundation.AsyncActionProgressHandler;
      Windows.Foundation.AsyncActionWithProgressCompletedHandler;
      Windows.Foundation.AsyncOperationProgressHandler;
      Windows.Foundation.AsyncOperationCompletedHandler;
      Windows.Foundation.AsyncOperationWithProgressCompletedHandler;
    </CsWinRTIncludes>
    <CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
    <CsWinRTWindowsMetadata>sdk</CsWinRTWindowsMetadata>
    <CsWinRTGenerateProjection>true</CsWinRTGenerateProjection>
  </PropertyGroup>
</Project>

However, because it generates public classes, but the WinRT.cs and WinRT_Interop.cs files have all types internal (but those types are exposed in the generated classes) I get the following build errors:,

`>\obj\Debug\net5.0\Generated Files\Windows.Foundation.Metadata.cs(30,16,30,36): error CS0051: Inconsistent accessibility: parameter type 'Platform' is less accessible than method 'ActivatableAttribute.ActivatableAttribute(uint, Platform)'
1>\obj\Debug\net5.0\Generated Files\Windows.Foundation.Metadata.cs(33,16,33,36): error CS0051: Inconsistent accessibility: parameter type 'Platform' is less accessible than method 'ActivatableAttribute.ActivatableAttribute(Type, uint, Platform)'
1>\obj\Debug\net5.0\Generated Files\Windows.Foundation.Metadata.cs(114,16,114,31): error CS0051: Inconsistent accessibility: parameter type 'Platform' is less accessible than method 'StaticAttribute.StaticAttribute(Type, uint, Platform)'

My main problem in the first place is that it generates public classes. I want them all to be internal, as I don't actually want to expose these methods, (doesn't seem like the source generator has that option) but just use them internally in a light-up way. There's just no reason to embed WinRT types and expose them in your class library - if you needed that in your app, just target the TFM that include WinRT APIs.

I then took the generated code from the obj folder and embedded them in a class library, and changed all public classes to internal. Now things actually do compile, but sadly I'm getting bad crashes at runtime, and in addition I'm forced to deploy a WinRT dependency as well (Microsoft.Windows.CsWinRT).

            WinRT.ComWrappersSupport.RegisterProjectionAssembly(typeof(Program).Assembly);
            Geolocator g = new Geolocator(); //OK
            g.DesiredAccuracy = PositionAccuracy.Default; // BOOM!

image

I have the use case of "the usage of the WinRT API is entirely internal to the library". I abstract the Bluetooth LE Stack (I have four implementations in the project) and WinRT is one of the implementations. Currently the TFM net5.0-windows.... of .NET is forcing itself into the application domain which I would prefer to have a single TFM (net5.0). I tried an AssemblyLoadContext Plugin approach which worked, however, the consequent packaging is just an unneeded hassle.

Embedded support is now available with C#/WinRT v1.4.1! These docs and samples provide more details on how to use the support.