Skip to content

Proposal: Handle multiple versioned WinRT packages loaded side-by-side in a process.  #153

@DefaultRyan

Description

@DefaultRyan

Versioned WinRT components side-by-side

This feature provides conventions for framework package authors to enabled loading multiple versions of a WinRT component side-by-side within a single process.

Background

When WinRT types were shipped as part of the OS, the versioning largely consisted of putting new functionality into a new interface, and not changing the previously shipped interfaces. When it came time to activate a runtimeclass, there was only one choice: the version of the class supplied by the OS.

As Project Reunion grows and evolves, more WinRT-based functionality will become available and consumed from framework packages shipped separately from the OS. In some ways, this can make versioning simpler. Because framework packages use a scheme whereby a different major version of a package is an entirely different package, I don't need to ensure that Widgets3 is ABI-compatible with Widgets2. (Gratuitous breaking changes are still hostile to developers, but updating your app to consume a newer version involves a recompile).

In other ways, however, this complicates the versioning story. In particular, MyApp might be consuming multiple components, each of which depend on a different version of a framework package. In short, something that looks like this:

MyApp.exe
|
+-SomeComponent.dll
| |
| +-Widgets2.dll (version 2 of Widgets)
|
+-OtherComponent.dll
  |
  +-Widgets3.dll (version 3 of Widgets)

Axiom 1: Framework packages will ship, make breaking changes, and ship again with a newer major version.

Axiom 2: It will sometimes be infeasible, if not impossible, for an app to update its dependencies so that all components can agree upon a single major version of a package.

If the types in different versions of a WinRT component have the same fully qualified names (i.e. namespace + type name), this will lead to ambiguities in various parts of the runtime.

All WinRT type activation goes through RoGetActivationFactory, and that API accepts a string holding the name of the activatable class - there is no way to supply the version or filename of an assembly or dll. This will introduce ambiguitiy to activation.

If C#/.NET attempts to load multiple versions of identically-named types from multiple assemblies, I've been told the developer is going to Have A Bad Time.

Description

A package author can resolve these ambiguities and issues by adopting a versioning convention for their type names. This can either be done manually, or simplified with enlightened tooling.

The crux of the approach is to suffix a type's namespace with a string containing the major version number.

This approach resolves ambiguities with activation because the type names involved are now unique. A big advantage is that this can work with existing tooling, today. But we can enlighten our tools to improve the developer experience.

We can enlighten the MIDL compiler to use this versioning scheme via a new command line option, so that it appends the namespace automatically using the value supplied to the ContractVersionAttribute or VersionAttribute.

Similarly, we can enlighten projection tools (cppwinrt.exe, abi.exe) to recognize this versioning scheme and reduce developer friction by eliminating the need to spell out the versioned namespaces.

Examples

Englightened toolchain

With an enlightened toolchain, a package author can use the Version (or ContractVersion) attribute to specify the package version.

namespace Widgets
{
    [version(3.0)]
    runtimeclass Doodad // Puts Doodad into the _v3_ namespace
    {
        // Methods
    }
}

An enlightened projection tool can then use its knowledge of this versioning scheme to improve the developer experience. For example, a C++ projection could use inline namespaces to "hoist" the latest version into the top-level namespace.

// C++/WinRT projection
namespace winrt::Widgets
{
    inline namespace _v3_
    {
        struct Doodad;
    }
    
    namespace _v2_
    {
        struct Doodad;
    }
}

winrt::Widgets::Doodad b; // Uses _v3_::Doodad
winrt::Widgets::_v2_::Doodad b2; // _v2_::Doodad is still reachable
winrt::Widgets::_v3_::Doodad b3; // _v3_::Doodad is also reachable

Unenlightened toolchain

Even with an unenlightened MIDL compiler, an author can manually add the namespace versioners.

namespace Widgets._v3_ // Version 3.x of Widgets
{
    runtimeclass Doodad
    {
        // Methods
    }
}

An unenlightened projection tool wouldn't be able to perform any namespace hoisting, but types would still be visible, and most languages of interest have mechanisms to remove some of the friction manually.

// C++/WinRT projection
namespace winrt::Widgets
{
    namespace _v3_
    {
        struct Doodad;
    }
    
    namespace _v2_
    {
        struct Doodad;
    }
}

// No projection magic, so add a "using namespace"
using namespace winrt::Widgets::_v3_;

winrt::Widgets::Doodad b; // Uses _v3_::Doodad
winrt::Widgets::_v2_::Doodad b2; // _v2_::Doodad is still reachable
winrt::Widgets::_v3_::Doodad b3; // _v3_::Doodad is also reachable

Other benefits

A feature worth noting is that if MIDL3 syntax is used so that the MIDL compiler automatically generates the interface UUIDs, the different namespace will force different UUIDs to be generated, preventing consumers from accidentally performing invalid conversions between the two versions.

winrt::Widgets::_v2_::Doodad f();
winrt::Widgets::_v3_::Doodad obj = f(); // Error

None of the above, however, precludes a package author from opting in and providing consumers the ability to convert between versioned types. This could take different forms, depending on the degree of interoperability desired by the author.

One approach would be for a newer version of a runtimeclass to implement the older versions of its interfaces, so that it responds to QueryInterface requests.

Another approach would be to provide an overloaded constructor or static method that could convert a newer versioned object (or wrapper) around an older object.

Remarks

Alternative approaches

Explicitly supply the desired DLL to activation

One considered approach would be for versioned WinRT types to use the same namespace between versions, and detour/augment RoGetActivationFactory so that callers would supply the desired version of the DLL. Something like:

HRESULT hr = RoGetActivationFactory(L"Widgets3.dll!Widgets.Doodad", iid, ppResult);

An appealing aspect of this approach is the apparent simplicity.However, this approach was rejected, because putting all the versioned WinRT types into the same namespace created many ambiguities:

  • C#/.NET present serious hurdles to get this to work. Even if it got "working", it lays plenty of traps and pitfalls to catch non-expert developers, who will doubtless wonder why their compiler says they can't convert from Widget.Doodad to Widget.Doodad.
  • If versioned types have the same name and namespace, things get very ugly whenever both types need to coexist in the source code for the same module. This is a situation that's easy to create if an app's components expose any APIs that use the versioned type:
// Component1.h
Widgets::Doodad foo(); // Uses Widgets 2.0


// Component2.h
Widgets::Doodad bar(); // Uses Widgets 3.0

// I've got a bad feeling about this...

What type is the object under the hood of the Doodad object? Is it v2 or v3? How would we even get this to compile without encountering "type redefinition" errors. Even if we did compile successfully, we'd be in a fragile state of affairs, flirting with runtime Undefined Behavior caused by violations of the "One Definition Rule".

Metadata

Metadata

Assignees

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions