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

WinAppSdk is difficult to use with reg-free WinRT due to class/file naming, pairs #2352

Closed
riverar opened this issue Apr 2, 2022 · 11 comments

Comments

@riverar
Copy link
Contributor

riverar commented Apr 2, 2022

Problem

Windows App SDK (WAS) 1.1-preview1 added the ability to ship as part of a self-contained application. What this generally means is that WAS components are now activatable without registration (registration-free WinRT). Instead of relying on registration information stored in the Windows Registry, WAS tooling now/currently stuffs a large manifest into your app replicating that function.

To support this feature outside Visual Studio/Msbuild C++/C#, developers must:

  • carefully author this manifest manually (or carry around a generic one and maintain it)
  • distribute it with their app internally (as a manifest resource) or externally (my.exe.manifest)

To avoid manifestation altogether, projections such as C++/WinRT and C#/WinRT alternatively turn to a simple algorithm to locate activatable classes and their activation factory. These projections:

  1. remove the class name (from the fully-qualified class name)
  2. look for a library with that name
  3. if that's not found, remove the most-specific segment name and repeat step 2

Unfortunately, WAS does not adhere to best WinRT component naming practices. WAS files have unexpected names and/or host classes that don't have related namespaces.

Here's a snippet of what the class <-> file relationship looks like today:

Library Classes
Microsoft.WindowsAppRuntime.dll Microsoft.Windows.ApplicationModel.DynamicDependency.PackageDependency
Microsoft.Windows.ApplicationModel.DynamicDependency.PackageDependencyRank
Microsoft.Windows.System.EnvironmentManager
...
Microsoft.UI.Xaml.dll Microsoft.UI.Xaml.Controls.DragItemsStartingEventArgs
Microsoft.UI.Xaml.Controls.ItemClickEventArgs
Microsoft.UI.Xaml.Controls.SelectionChangedEventArgs
Microsoft.UI.Xaml.Controls.SemanticZoomViewChangedEventArgs
...
Microsoft.UI.Xaml.Controls.dll Microsoft.UI.Xaml.XamlTypeInfo.XamlControlsXamlMetaDataProvider
Microsoft.UI.Xaml.Controls.XamlControlsResources
Microsoft.UI.Private.Controls.MUXControlsTestHooks
Microsoft.UI.Xaml.Controls.Primitives.CornerRadiusToThicknessConverter
Microsoft.UI.Xaml.Controls.Primitives.CornerRadiusFilterConverter
...
WinUIEdit.dll Microsoft.UI.Text.FontWeights
Microsoft.UI.Text.TextConstants
Microsoft.Web.WebView2.Core.dll Microsoft.Web.WebView2.Core.CoreWebView2ControllerWindowReference
Microsoft.Web.WebView2.Core.CoreWebView2Environment
Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions
CoreMessagingXP.dll Microsoft.UI.Dispatching.DispatcherQueue
Microsoft.UI.Dispatching.DispatcherQueueController
...

(The full table, as of 1.1-preview1, can be found here.)

Observe in the above snippet:

  • Microsoft.WindowsAppRuntime.dll hosts classes in the Microsoft.Windows.ApplicationModel.* and Microsoft.Windows.System.* namespaces
  • Both Microsoft.UI.Xaml.dll and Microsoft.UI.Xaml.Controls.dll host classes in the Microsoft.UI.Xaml.Controls.* namespace
  • WinUIEdit.dll hosts classes in the Microsoft.UI.Text namespace
  • etc.

I explored the possibility of simply renaming the libraries to compatible names, but Microsoft.UI.dll and Microsoft.UI.Xaml.dll both contain types that would fail the aforementioned search algorithm.

Considered workarounds/solutions

  1. Rename all WAS libraries and move the classes to the right spots.
    Requires WAS code change. Could be considered a major change (2.0).

  2. Continue using the internal/external manifest.
    This does not follow WinRT component recommendations, is generally non-idiomatic, and is unfriendly for some projections (e.g. Rust).

  3. Change language projections to deal with non-ideal layout.
    Requires all projections to change. Involves working with sensitive projection code.

    Projections could perhaps:

    • consume the manifest (oracle)
    • rename the libraries, creating a saner layout, adding numbers to "dupes" (e.g. UI.Xaml.1, UI.Xaml.2, ...)
    • adjust activation factory search algorithm as needed
  4. Did I miss any?

@DrusTheAxe
Copy link
Member

TL;DR WinAppSDK uses #2 ByDesign, for performance and complexity reasons. What you call "non-ideal layout" implies the projections' resolution is ideal, which isn't true. It's simple, but not necessarily best. As in WinAppSDK's case.

#1 is hugely invasively and doesn't solve all the problems. #3 is a projection-specific answer, requires new designs and complicated new machinery, and doesn't solve all the problems without devolving to #1 but with more work.

We discussed this at length last year before deciding to embrace manifested solutions instead of language projection's manifest-free option. Not all component authors face the same choices so YMM but, for WinAppSDK, this is an intentional decision for good reasons.

Before discussing further it helps to understand the options available and their tradeoffs.

There are 3 ways to resolve a packaged WinRT's activatable class id (ACID):

A. Via the process' package graph. Requires using WinAppSDK/MSIX and the ACID defined in WinAppSDK's appxmanifest.xml
B. Via RegFreeWinRT. Requires the ACID defined in the caller's SxS (Fusion) manifest and the DLL found via LoadLibrary
C. Via language projections. C#/WinRT and C++/WinRT support the same "we're resolve it ourselves" option crawling the filesystem looking for a DLL containing the implementation. Requires caller uses a language project (C#/WinRT, C++/WinRT, etc), the WinRT runtimeclass implementation is in a DLL and the DLL's filename matching the ACID's namespace/prefix.

Your option #1 assumes resolution C. Caller uses OS mechanisms as well as its own hunting for a matching file, where the filename is named the same as the runtimeclass' namespace or prefix, e.g. C#/WinRT and C++/WinRT can find a theoretical connection object with the ACID Microsoft.Network.Bluetooth.Connection by calling...

  • LoadLibrary("Microsoft.Network.Bluetooth.Connection.dll")
  • LoadLibrary("Microsoft.Network.Bluetooth.dll")
  • LoadLibrary("Microsoft.Network.dll")
  • LoadLibrary("Microsoft.dll")

until 1st success or none match and not found.

This is sometimes referred to as the 'manifest-free' option.

The downsides are performance, required filenames and non-trivial objects (in both breadth and depth).

  • Performance - this crawls the filesystem hunting for a matching DLL filename. Though the implementations differ filesystem access is orders magnitude slower than the manifested lookups.
  • Required Filenames - a WinRT object's hosting DLL's filename becomes part of the de facto contract; you can't put runtime A.Foo in just any file. Unlike WinRT (and classic COM) the host's filename and location are no longer artifacts of implementation but part of the de facto API contracct. In addition, the filename/namespace rule poses challenges in both breadth and depth
    • Breadth -- it's not possible to put widgets with ACID A.Foo and B.Foo in the same DLL, even if their implementation would be best implemented in the same host (for a variety of reasons). It is possible to put A.X.Foo and A.Y.Foo in the same DLL if-and-only-if it's the common root A.dll
    • Depth -- deep hierarchies are forced to use the common root. For instance, the language projection hunt sequence requires Microsoft.UI.Colors exist in Microsoft.UI.dll or Microsoft.dll. Likewise, Miccrosoft.UI.XAML.Controls.Button could be found in Microsoft.UI.XAML.Controls.dll, Microsoft.UI.XAML.dll, Microsoft.UI.dll or Microsoft.dll. But having both doesn't work TL;DR this would require WinUI (XAML, Composition, Input, more) all in the same DLL. That's seriously sub-optimal for both producing (us) and consuming (you). WinAppSDK would need to only use namespaces 3+ levels deep. At first blush this might seem manageable for smaller feature areas and problem spaces such as Microsoft.Windows.AppLifecycle or Microsoft.Windows.ApplicationModel.DynamicDependency it would force Microsoft.WindowsAppRuntime.dll to explode into many DLLs (8 today. And the days are young...). This becomes even more daunting when you consider larger object models like WinUI and Composition where moving some types across namespaces would be necessary simply to work at all (e.g. see Colors) and the compatibility issues that would impose for any migration of existing WinUI2 and System XAML users to WinAppSDK.

The manifest-free solution is convenient for simple cases, and likewise the object model and performance impacts are small. Larger more complex designs don't far so well with the manifest-free option and better suited to the manifested options.

MSIX's appxmanifest.xml is the optimal solution. Not only optimal performance but also the most convenient and reliable - you use WinAppSDK and ItJustWorks™, because the component author (in this case, us) did the work needed for the components to be consumed (by you), and any code changes impacting that come hand-in-hand.

Alas, the WinAppSDK/Self-Contained model makes that solution a non-option. We know the ACIDs/DLLs layout and can define those relations in metadata (application.manifest) it's just a SxS manifest you the component consumer must ultimately provide at runtime with our information. Distasteful, but workable.

NOTE: You don't need to provide a single application.manifest -- mt.exe is smart enough to accept multiple manifests as input and smartly merge their content. Our NuGet and provided templates should provide this behavior. You can still define your own application.manifest for your own reasons (dpiAwareness, msix, etc) and even define other non-WinAppSDK RegFreeWinRT objects, and have the appropriate info merged into the final linked binary.

And that's why WinAppSDK uses manifested solutions for its WinRT components. Other libraries may reach different decisions as they face different tradeoffs.

Did I miss any?

Use WinAppSDK via MSIX packages. Then no need for RegFreeWinRT thus no problemo :-)

@riverar
Copy link
Contributor Author

riverar commented Apr 4, 2022

Thanks, sounds like this is intentional then. Will close.

My guidance to language projection authors will be to embed the manifest (if they have the tooling to do so) or parse the manifest and deal with the class id<->file map at codegen time. I'm working on the latter approach with the windows-app rust crate at the moment.

@riverar riverar closed this as completed Apr 4, 2022
@DrusTheAxe
Copy link
Member

DrusTheAxe commented Apr 4, 2022

My guidance to language projection authors will be to embed the manifest (if they have the tooling to do so) or parse the manifest and deal with the class id<->file map at codegen time. I'm working on the latter approach with the windows-app rust crate at the moment.

I'd advise caution. The downside to the SxS manifest is the 'registration info' is separate from the implementation. Changes to the implementation can impact the registration info, e.g. if v1.2.3 adds a new runtimeclass not in v1.2.2. If/when we use internal runtimeclasses amongst WinAppSDK components not exposed as public APIs, you need to use the same registration info as the matching implementation. Incomplete or mismatched registration info vs runtime can produce 'undefined behavior'.

I forgot to ask but thanks for reminding me...

  1. Continue using the internal/external manifest. This does not follow WinRT component recommendations

What WinRT component recommendations are you referring to? Got link?

and is unfriendly for some projections (e.g. Rust).

Can you elaborate?

@riverar
Copy link
Contributor Author

riverar commented Apr 4, 2022

I'd advise caution. The downside to the SxS manifest is the 'registration info' is separate from the implementation. Changes to the implementation can impact the registration info, e.g. if v1.2.3 adds a new runtimeclass not in v1.2.2. If/when we use internal runtimeclasses amongst WinAppSDK components not exposed as public APIs, you need to use the same registration info as the matching implementation

I think you're warning folks to ensure they keep up on manifests. That's a fair warning and something I automate (for Rust). Every time I regenerate the crate for a major/minor/bugfix, we will retrieve a fresh copy. Sound good?

What WinRT component recommendations are you referring to? Got link?

It's linked at the top, but easy to miss. Here's another copy. (Admittedly, this is in the context of C++/WinRT, which you addressed above.)

Can you elaborate?

There's no easy/portable/idiomatic way to copy the manifest around (or embed it into the image) at this time. It's a known gap in the ecosystem right now. 😞

@DrusTheAxe
Copy link
Member

I think you're warning folks to ensure they keep up on manifests

More like not to forget the manifest is tightly coupled with and part of the implementation. If you're using WinAppSDK's nuget and MSbuild support ItJustWorks™. If you're coloring outside the lines make sure to color all the lines ;-)

Every time I regenerate the crate for a major/minor/bugfix, we will retrieve a fresh copy. Sound good?

Yes. Just noting as the manifest is something easily overlooked.

Here's another copy. (Admittedly, this is in the context of C++/WinRT, which you addressed above.)

Thanks! I see that doesn't mention the complex case or the namespace-depth concern. We should correct that. And by we I mean...

@BenJKuhn we should expand the guidance for the complex cases (like WinAppSDK :-) that don't fit so well with the simple filename/lookup scheme. https://task.ms/38821629

There's no easy/portable/idiomatic way to copy the manifest around (or embed it into the image) at this time. It's a known gap in the ecosystem right now. 😞

Which ecosystem, Rust or Windows?

@kennykerr
Copy link
Contributor

I'm not sure how helpful it would be to update the docs to cover WinAppSDK's implementation details - I can't imagine we'd really recommend anyone else building something this complex as a best practice, which is what the docs are talking about.

@riverar
Copy link
Contributor Author

riverar commented Apr 4, 2022

Which ecosystem, Rust or Windows?

Rust.

@DrusTheAxe
Copy link
Member

I'm not sure how helpful it would be to update the docs to cover WinAppSDK's implementation details

Not the ask. WinAppSDK is merely an instance of a complex implementation not served as well by the manifest-free guidance as a manifested one. We should document guidance for component authors not in the 'simple' case.

I can't imagine we'd really recommend anyone else building something this complex as a best practice

Office. Photoshop. SAP. Lots other realworld codebases don't fit so well in the manifest-free model. I'm not saying we recommend developers write complex systems but it happens. When they do, we should have guidance (other than "rewrite your complex implementation to fit the manifest-free model").

@DrusTheAxe
Copy link
Member

Which ecosystem, Rust or Windows?

Rust.

:-(

Rust crates have no convention for it? Yet? ;-)

@kennykerr
Copy link
Contributor

rust-lang/cargo#9661

@Lexikos
Copy link

Lexikos commented Jun 11, 2022

Your option #1 assumes resolution C.

Is there some reason that resolutions A and B wouldn't work just as well with option 1 as with the existing layout? If I understand correctly, the manifest avoids the need to crawl the filesystem, regardless of whether the class/file relationship supports registration-free activation or not. In other words, resolution C carries the performance penalty, not option 1.

... you can't put runtime A.Foo in just any file.

Is that the class implementation, the activation factory for that class, the metadata...?

My understanding is that registration-free activation finds the DLL by relying on a naming convention, loads it and calls the DllGetActivationFactory entry point to get an activation factory. So the only real requirement is that the DLL is able to locate the activation factory for the given runtime class. The class implementation itself can be anywhere. Even the activation factory can be implemented elsewhere, as long as the entry point knows how to locate it. The DLL could be just a stub used for registration-free activation, perhaps comparable to an API set.

However, I'm likely missing something. For instance, I don't know how the metadata is located for a third-party component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants