The journey of moving from C++/WinRT to C# in the Microsoft Store

Sergio Pedri

Microsoft Store

The new Microsoft Store is a native Windows application, written in C#/XAML, running on the UWP framework. Along with building some of the awesome features the new Store has (such as support for unpackaged and Android™ apps, the new pop-up Store, etc.), we’ve been spending a lot of time optimizing, refactoring and modernizing the codebase, with the goal to make the app faster and easier to maintain.

Part of this effort has involved moving the few bits of C++/WinRT code we had to C#. Doing so brings several advantages, particularly to the dev inner loop:

  • Build times become smaller (eg. the C++ compiler needs to parse and process header files, which can be quite expensive). For instance, building a very small C++/WinRT project with just a handful of files can take ~13 seconds, whereas building a very small C# class library will usually take less than 0.2 seconds.
  • Having only C# projects makes setting up a new dev machine easier: you’d just need to install the basic .NET and UWP workloads, without having to worry about also pulling in the right VC++ dependencies to build the C++/WinRT projects.
  • The development experience with C# is generally better (especially when modifying files and trying things out), with Roslyn happily giving proper error messages to guide you. With C++/WinRT instead, it’s not uncommon to always have false positive errors showing up and red squiggly lines all over the place (especially when touching code that spans .idl files and .h/.cpp files) even when the code is actually correct and compiling just fine.
  • Having all code in C# just makes the codebase “more consistent” as a whole and easier to work with, especially for people that might have experience with C# but might not be particularly familiar with C++/WinRT.

As part of the ongoing effort to get to a purely managed, 100% C# codebase for the Microsoft Store, one of the latest changes we did was to migrate our APIs to interact with Windows Package Manager (more on this below) from C++/WinRT. Doing so ended up being more challenging than anticipated, and it was also a great opportunity for us to learn something new about how all the various technologies involved work together (C++/WinRT, .NET interop, .NET Native, COM, etc.).

As such, we wanted to share with the whole community this little adventure we embarked on to migrate these Windows Package Manager APIs from C++/WinRT to C#. The topic and APIs involved in this blog post on their own are very specific, but we felt the story on its own would be an interesting case study to introduce many of the concepts involved.

So, here’s how we got the Microsoft Store to use C# to access Windows Package Manager! 🍿

What is Windows Package Manager?

Windows Package Manager is Windows’ own built-in package management system, commonly accessed via its CLI tool winget. The Store client accesses Windows Package Manager programmatically (not via winget) and uses it to install unpackaged apps (ie. classic Win32 applications). This is in contrast to packaged apps (ie. UWP, PWA, MSIX-packaged Win32 applications), which rely on Windows Update instead. This is why Win32 apps installed from the Store then also show up when using the winget list command, as the same backend is taking care of installing them, even if the Store client was being used instead of the CLI tool (note the “msstore” source):

winget list

Usage in the Store client

The way the Store client accesses the Windows Package Manager functionality is via the APIs from the Microsoft.Management.Deployment namespace, which are exposed from a .winmd file from a NuGet package we’re referencing. This assembly contains WinRT interop APIs to access Windows Package Manager via its COM interfaces. The source for that package is available on GitHub.

The APIs from this assembly are internally used in our WpmInstallerShim type, which contains a thin abstraction over the various Windows Package Manager APIs. This type is responsible for creating instances of the various objects being used (eg. PackageManager) and then checking the level of available features, starting installations, monitoring progress, cancelling installations, etc.

It’s important to note that the referenced assembly does not contain an implementation of the exposed APIs. In other words, you wouldn’t eg. just do new PackageManager() as that wouldn’t work, because that method is not implemented. The way instances are retrieved is by creating them as remote objects from a local COM server (ie. another process), so that they can access the shared functionality from the OS and do their work. This aspect is what made the transition from C++/WinRT to C# particularly tricky, and why the current architecture is structured the way it is.

The rest of this post will go over what the original C++/WinRT implementation used to be, how it was ported to C# and what kind of issues we’ve encountered and why, and finally how the problem has been resolved.

Technical implementation

Originally, Windows Package Manager APIs were accessed from a C++/WinRT project, which as we just mentioned was responsible for creating these remote objects and performing basic operations on them. In other words, the whole WpmInstallerShim class used to be a C++/WinRT type. Most of that logic working on instantiated Windows Package Manager types could be done in C# as well, but the crucial part that used to necessarily be done in C++/WinRT was this:

#include "pch.h"
#include "WpmInstallerShim.h"
#include "WpmInstallerShim.g.cpp"

const CLSID CLSID_PackageManager = { 0xC53A4F16, 0x787E, 0x42A4, 0xB3, 0x04, 0x29, 0xEF, 0xFB, 0x4B, 0xF5, 0x97 };

PackageManager CreatePackageManager()
{
    return winrt::create_instance<PackageManager>(CLSID_PackageManager, CLSCTX_LOCAL_SERVER);
}

The crucial bit is that winrt::create_instance call, which is creating an instance of type T (in this case, PackageManager). That method requires a CLSID parameter, which is the unique identifier for this specific type we want to instantiate (see here for more info), and the CLSCTX_LOCAL_SERVER flag. This is the critical bit: that value from the CLSCTX enum is indicating that the object to create will be in a local COM server, meaning it will live in another process on the same machine (thus being a remote object). Using that option is necessary to make the whole thing work because, as we said, we don’t actually have the implementation of these types in our own process.

This is the core component that needed to be ported to C# in order to remove the C++/WinRT dependency here, and most importantly, had to run fine on .NET Native (which has additional constraints and limitations on top of what C# has on its own).

Now, the winrt::create_instance API is not available in C#, but we can take a look at what it does behind the scenes in order to port it. Simplifying things a bit, that API can be seen as conceptually doing:

PackageManager* pPackageManager;

if (CoCreateInstance(
    CLSID_PackageManager,
    null,
    CLSCTX_LOCAL_SERVER,
    __uuidof(PackageManager),
    &pPackageManager) != S_OK)
{
    // throw...
}

return pPackageManager;

That is, it would (again, conceptually, this is not how it is actually implemented) first use the __uuidof compiler extension, which just retrieves the GUID of the input type, and then pass that as parameter to CoCreateInstance. This API is essentially the main API to instantiate all COM objects (remember that WinRT objects are also COM objects). This is perfect, as this is something we have access to from C#, and that we can port. We might do something like this (and in fact, this is what we did at first).

NOTE: to make the following code snippets simpler, we’ll assume we have already defined the necessary primitive types we’re using, such as ComPtr<T> and IUnknown. If you’d like more info on how they work or how they’re implemented, you can see the C# port of them here and here, as well as the official docs here and here.

For a very small introduction, just consider that IUnknown is the base interface implemented by all COM objects, which provides methods to add and remove a reference (necessary for lifetime management, see reference counting), whereas ComPtr<T> is a very thin wrapper around a T* pointer (where T : IUnknown), which offers helpers methods such as automatically releasing the underlying object when the ComPtr<T> variables goes out of scope. Essentially, you can think of this as being able to have IDisposable for COM objects.

With that said, here’s what a first C# port of the Windows Package Manager instantiation could look (and did look) like:

private static readonly Guid CLSID_PackageManager = new(0xC53A4F16, 0x787E, 0x42A4, 0xB3, 0x04, 0x29, 0xEF, 0xFB, 0x4B, 0xF5, 0x97);

private static readonly Guid IID_IPackageManager = new(0xB375E3B9, 0xF2E0, 0x5C93, 0x87, 0xA7, 0xB6, 0x74, 0x97, 0xF7, 0xE5, 0x93);

[DllImport("ole32", ExactSpelling = true)]
public static extern uint CoCreateInstance(Guid* rclsid, void* pUnkOuter, uint dwClsContext, Guid* riid, void** ppv);

public static T CreateInstanceFromLocalServer<T>(Guid classId, Guid interfaceId)
    where T : class
{
    using ComPtr<IUnknown> result = default;

    uint hresult = ComBaseApi.CoCreateInstance(
        rclsid: &classId,
        pUnkOuter: null,
        dwClsContext: (uint)CLSCTX.CLSCTX_LOCAL_SERVER,
        riid: &interfaceId,
        ppv: (void**)result.GetAddressOf());

    if (hresult != S.S_OK)
    {
        Marshal.ThrowExceptionForHR((int)hresult);
    }

    return (T)Marshal.GetObjectForIUnknown((IntPtr)result.Get());
}

First, we’re declaring a P/Invoke for the native CoCreateInstance we need, invoking it with the same parameters as the C++/WinRT code. However, here we’re manually passing the GUID of the first interface implemented by PackageManager instead of relying on winrt::create_instance automatically looking the GUID up for us.

Next, we marshal the created object back via Marshal.GetObjectForIUnknown. This API essentially takes a pointer to a COM object (ie. any objects that implements the IUnknown interface), and marshals it to a managed object. Specifically, it creates a RCW (Runtime Callable Wrapper), which is a special managed object that internally tracks the native COM object being wrapped.

Finally, we’re casting to T, as we’re relying on the runtime being able to recognize that the input COM object was a specific type from that .winmd file we’re referencing (eg. PackageManager in this case) and hence creating the managed object of exactly that type, so we can then cast it and use it directly.

And… This works! 🎉

…On .NET Core. But, unfortunately, not on .NET Native. 💥

What is .NET Native?

Let’s now pause to make a small recap of what .NET Native is, for those of you that might not be aware of it. If your mind went to NativeAOT, well, close, but not exactly the same.

.NET Native is the compiler toolchain used by all .NET UWP apps (which are distributed in the Microsoft Store as native binaries, running on this infrastructure). Put simply, .NET Native combines a custom .NET runtime (and accompanying BCL) that is optimized for AOT with a series of build steps that eventually have all application code go through a modified version of the Microsoft VC++ compiler. The final result is an application package being 100% AOT and having great performance and memory usage. For a more in-depth overview of how this works, I highly recommend reading this blog post by Xy Ziemba on the topic.

The fact that UWP applications run on .NET Native when distributed is generally transparent to application developers, in that the behavior of the .NET Native runtime is usually identical to that of the custom .NET Core runtime that is used for Debug builds. For instance, UWP applications can reference NuGet packages not only targeting UWP specifically, but .NET Standard 2.0 as well. Since the UWP framework implements all APIs for that contract, those libraries will just work out of the box, with their authors not having to know anything about UWP or what specific runtimes will actually run that code. It is the same concept that allows .NET Standard libraries to also be used on other runtimes such as .NET Framework, Mono, Unity, and more.

But, there are some cases where visible differences can manifest, and that is especially true when doing interop and working with COM or WinRT APIs (as is the case here). This can be both due to some inherent differences in the .NET Native runtime, as well as the way MCG (Marshal Code Generator, more on this in the .NET Native blog post mentioned above) will interact with code doing interop, which can sometimes contribute to functional differences in Debug/Release builds.

Why is this code failing there?

It turns out, that the way .NET Native handles marshalling is different than .NET Core. In this scenario, it’s not able to correctly recognize and create the right target object automatically for us, and it needs callers to provide additional information about what type they actually want to use to marshal the native object back to the managed world.

But, the C++/WinRT code was in fact working fine. What if, as a temporary solution, we moved everything to C#, but kept the actual object instantiation in C++/WinRT, since that’s not causing any problems? Essentially, the C# side could then just do:

PackageManager packageManager = WpmInstallerShim.CreatePackageManager();

Then we can use the marshalled instance just fine. Upon testing, sure enough, this does indeed work. But if this works, then it begs the question: does this not mean that .NET Native is somehow marshalling the native object returned by the C++/WinRT component back to the managed world, as a PackageManager instance?

If so, then can’t we just use whatever mechanism it’s using too to replicate the same behavior, but while porting the object creation too from C++/WinRT to C#? In order to do so, we can inspect the actual code that .NET Native is using. Crucially, since this is a call to a WinRT component, this means that the code responsible for the marshalling is not actual “runtime magic”, but code being produced at compile time by MCG.

As we said, MCG is one of the tools in the .NET Native toolchain, and it’s responsible for pregenerating all interop and marshalling code ahead of time, for better performance (more info on this process in the same .NET Native blog post mentioned earlier). The most important thing is that all code produced by MCG is generated to physical files on disk which we can easily inspect. Specifically, all MCG-generated code for WinRT interop goes into an ImplTypes.g.cs file in the ilc folder of the build artifacts.

With some digging, we can see what the code for that CreatePackageManager call looked like:

public static global::Microsoft.Management.Deployment.PackageManager CreatePackageManager(global::System.__ComObject __this)
{
    global::Microsoft.Management.Deployment.PackageManager __ret =
        global::__Interop.ForwardComStubs.Stub_3<global::WinStore.Native.IWpmInstallerShimStatics, global::Microsoft.Management.Deployment.PackageManager>(
            __this, 
            6);

    return __ret;
}

So, this is the generated code that the call to the C++/WinRT WpmInstallerShim.CreatePackageManager() callsite is redirected to. This is just invoking the generated stub, so nothing to see here. Whatever magic is making things work, it’ll be in that ForwardComStubs.Stub_3 method, so let’s take a look at that one. This time, that’ll be in SharedStubs.g.cs. Here it is:

[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
internal static TResult Stub_3<TThis, TResult>(
    global::System.__ComObject __this, 
    int __targetIndex)
{
    // Setup
    void* unsafe_TResult__retval = default(void*);
    TResult TResult__retval = default(TResult);
    int unsafe___hr;

    try
    {
        // Marshalling
        unsafe_TResult__retval = null;

        // Call to native method
        unsafe___hr = global::__Interop.ComCallHelpers.Call(
            __this, 
            typeof(TThis).TypeHandle, 
            __targetIndex, 
            &(unsafe_TResult__retval));

        TResult__retval = (TResult)global::System.Runtime.InteropServices.McgMarshal.ComInterfaceToObject(
            ((global::System.IntPtr)unsafe_TResult__retval), 
            typeof(TResult).TypeHandle);

        // Return
        return TResult__retval;
    }
    finally
    {
        // Cleanup
        global::System.Runtime.InteropServices.McgMarshal.ComSafeRelease(new global::System.IntPtr(((void*)unsafe_TResult__retval)));
    }
}

Now, this is interesting. We can see the generated code is doing nearly the same thing we were doing: there is a small stub that invokes the native WinRT API, which returns a raw pointer to the instantiated COM object (so the same thing we could also create on the C# side by just P/Invoking CoCreateInstance), and then a marshalling call to create a managed wrapper (in this case, a PackageManager instance) from that COM object.

There is additional complexity here. The stub is generic so that it can be shared for multiple APIs with a similar signature but working on different types. That doesn’t change the core logic we’re interested in, though. But, and this is what’s making all the difference, Marshal.GetObjectForIUnknown is not actually being used here. Instead, MCG is generating a call to McgMarshal.ComInterfaceToObject. This is a special helper API that essentially does the same as Marshal.GetObjectForIUnknwn, but also taking a RuntimeTypeHandle value representing the exact target type to marshal the object to. This is what allows the internal implementation to correctly marshal the object to the type the caller was expecting, and it was what our original code was missing.

Unfortunately, McgMarshal.ComInterfaceToObject is an internal API, so we can’t use it. What’s worse, this API is not even just an internal API in the .NET Native corelib (like eg. other internal APIs in the runtime), but it instead lives in a private assembly (System.Private.Interop) which is referenced in a special way during the compilation process. This makes it so that it’s completely unaccessible to everyone else but MCG-generated code and the runtime itself. In other words, even trying to load this up via reflection yields no result: the API is just not meant to be visible to any code that wasn’t generated by MCG.

It doesn’t matter whether the application is preserving reflection metadata for the whole package or not: .NET Native will still strip metadata from its private assemblies as it assumes that no reflection will ever be done on them, so even retrieving those types through the set of loaded assemblies for the current app domain results in no defined types being reported. Additionally, even if the reflection approach had worked, it would’ve only been useful as a test and not as a proper solution anyway. We wouldn’t have wanted the final code to rely on reflection to work at all, as it would’ve meant it would’ve been particularly brittle.

So now, we know what the last piece of the puzzle to complete our fully managed port is: McgMarshal.ComInterfaceToObject. But, we cannot use it, as that method can only be accessed by MCG-generated code. What if… Instead of trying to call that API ourselves, we managed to instead find a way to get MCG to call it for us, but with the right signature and callsite we needed, while also keeping everything in C#? That sounds promising, but how could we do that?

Let’s go back to our CoCreateInstance P/Invoke declaration:

[DllImport("ole32", ExactSpelling = true)]
public static extern uint CoCreateInstance(Guid* rclsid, void* pUnkOuter, uint dwClsContext, Guid* riid, void** ppv);

This declaration is using a blittable signature (ie. all parameters as well as the return type are blittable types, meaning no marshalling is necessary, see docs for more info).

What if we deliberately did not use a blittable signature instead, to force MCG to actually generate our marshalling call for us? In other words, since we need each marshalling call to also pass the specific runtime type handle for the type we want to marshal our COM objects to, what if we declared multiple overloads of this P/Invoke, each with the same entry point (the actual CoCreateInstance API), but each returning a managed object of the target type we need? That is, what about:

[DllImport("ole32", EntryPoint = "CoCreateInstance", ExactSpelling = true)]
private static extern uint CoCreatePackageManagerInstance(Guid* rclsid, void* pUnkOuter, uint dwClsContext, Guid* riid, out PackageManager ppv);

// Other overloads for all other Windows Package Manager types below...

This P/Invoke has the same signature, but we replaced that void** parameter with an out PackageManager parameter. Let’s see what MCG will generate for this (all P/Invoke calls are also redirected by .NET Native, just like WinRT calls, and as such they all go through MCG too). This time, the call will be in PInvoke.g.cs:

public unsafe static partial class ole32_PInvokes
{
    [global::System.Runtime.InteropServices.DllImport("ole32", EntryPoint="CoCreateInstance", CallingConvention=global::System.Runtime.InteropServices.CallingConvention.Winapi)]
    public extern static uint CoCreatePackageManagerInstance(
        global::System.Guid* rclsid, 
        void* pUnkOuter, 
        uint dwClsContext, 
        global::System.Guid* riid, 
        void** ppv);
}

First, we can see MCG is generating the same blittable P/Invoke declaration we had before, for each of our overloads. This makes perfect sense, as it still needs to invoke this native API in the generated marshalling stub. So let’s take a look at what that looks like:

[global::System.Runtime.InteropServices.McgPInvokeMarshalStub("WinStore.Core, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "WinStore.Acquisition.PackageManager.WpmInstallerShim", "CoCreatePackageManagerInstance")]
public static uint CoCreatePackageManagerInstance(
    global::System.Guid* rclsid, 
    void* pUnkOuter, 
    uint dwClsContext, 
    global::System.Guid* riid, 
    out global::Microsoft.Management.Deployment.PackageManager ppv)
{
    // Setup
    void* unsafe_ppv = default(void*);
    uint unsafe___value;

    try
    {
        // Marshalling
        unsafe_ppv = null;

        // Call to native method
        unsafe___value = global::__Interop.ole32_PInvokes.CoCreatePackageManagerInstance(
            ((global::System.Guid*)rclsid), 
            ((void*)pUnkOuter), 
            dwClsContext, 
            ((global::System.Guid*)riid), 
            &(unsafe_ppv));

        ppv = (global::Microsoft.Management.Deployment.PackageManager)global::System.Runtime.InteropServices.McgMarshal.ComInterfaceToObject(
            ((global::System.IntPtr)unsafe_ppv), 
            typeof(global::Microsoft.Management.Deployment.PackageManager).TypeHandle);

        // Return
        return unsafe___value;
    }
    finally
    {
        // Cleanup
        global::System.Runtime.InteropServices.McgMarshal.ComSafeRelease(new global::System.IntPtr(((void*)unsafe_ppv)));
    }
}

And there we have it! We can see this is exactly the code we wanted to generate by hand, with the same native CoCreateInstance call, and then that McgMarshal.ComInterfaceToObject call passing typeof(PackageManager).TypeHandle as the additional argument, and correctly getting the runtime to generate a managed PackageManager instance for us. Awesome! 🥳

And to finish things up and reduce duplication, we can then introduce a small delegate type to take in these factories, so we can still offer a generic factory method for all of these managed types, which will handle passing the right arguments to CoCreateInstance, as well as error handling:

public unsafe delegate uint CoCreateFactory<T>(Guid* rclsid, void* pUnkOuter, uint dwClsContext, Guid* riid, out T ppv)
    where T : class;

public static T CreateInstanceFromLocalServer<T>(Guid classId, Guid interfaceId, CoCreateFactory<T> factory)
    where T : class
{
    uint hresult = factory(
        rclsid: &classId,
        pUnkOuter: null,
        dwClsContext: (uint)CLSCTX.CLSCTX_LOCAL_SERVER,
        riid: &interfaceId,
        ppv: out T result);

    if (hresult != S.S_OK)
    {
        Marshal.ThrowExceptionForHR((int)hresult);
    }

    return result;
}

And with this, each object creation call from the (now managed!) WpmInstallerShim type will now look like this:

private static PackageManager CreatePackageManager()
{
    return ComMarshaller.CreateInstanceFromLocalServer<PackageManager>(CLSID_PackageManager, IID_IPackageManager, CoCreatePackageManagerInstance);
}

Just one line of code, reusing the same shared infrastructure, and only having to declare the matching CoCreateInstance overload.

As an additional bonus, it turns out that the CoCreateInstance call was actually failing to resolve and create the object by GUID (due to it being in a separate process and not actually visible to the UWP application for several reasons), so it was falling back to MBM (metadata-based marshalling) to actually instantiate it (see more info here). This was happening in the original winrt::create_instance call in C++/WinRT as well, not just in C#.

This means that we don’t even need to pass an actual interface GUID at all to create these objects. All that’s needed is to pass some GUID for an interface that the object does implement (as that’s the CoCreateInstance contract). And what is an interface that all of these objects will definitely implement? Of course, that’s IUnknown. Which then means, we don’t even have to declare and use different GUIDs for each different Windows Package Manager object we want to instantiate, we can just pass IID_IUnknown to all those calls, and MBM will still get our objects created just fine!

…On .NET Native. But, unfortunately, not on .NET Core. 💥

Yes this is the same issue as before, this time the other way around though.

Turns out, doing so does work fine in .NET Native (which makes sense: CoCreateInstance only requires the IID of an interface implemented by the COM object to create, and IUnknown is one), but on .NET Core it results in an access violation exception whenever one of the APIs on the Windows Package Manager types is invoked.

One theory we have to explain this (credits to Jeremy Koritzinsky from the .NET team for this one) is that since metadata-based marshalling is a new COM feature, the version of CoreCLR (.NET Core’s runtime) used on UWP might have no specific handling to support it, which would mean the .NET Core-based Debug toolchain might not be passing the right flags or setting things up correctly for it to trigger. After all, MBM is both a relatively new feature and also just meant for a corner case scenario in the first place (custom out-of-process WinRT servers). As a result, it might be the proxy stubs in Debug builds weren’t set up correctly and failed when trying to call through them at the first usage of the WinRT interface.

That would explain why calls to those WinRT APIs resulted in an access violation, whereas just marshalling the native objects back to managed instances did seem to work still. Not a big deal, we’ll just keep passing those IIDs for the various internal Windows Package Manager interfaces, with a note about this peculiar behavior in Debug builds if only the IUnknown IID is used.

And with that, we’ve gotten to what our Windows Package Manager interop code currently looks like! This is now entirely written in C#, and it will power all unpackaged app installs in the Microsoft Store going forward 🚀

Final thoughts

As we mentioned, the exact APIs being used here are quite specific to the Store, but we hope this post might just be an interesting story on its own about doing COM/WinRT interop in the .NET world and about the challenges it can pose.

We also hope that developers reading this that might not have been familiar with some of the topics being mentioned might have learnt something new as well. We certainly did learn quite a few things along the way:

  • C# and .NET are extremely capable with respect to native interop, even in niche scenarios. If there is some code that has been written in C++ because “it’s not possible to do that in C#”, chances are it is actually possible to do that C#, it might just require some time and effort to figure out how to do that exactly.
  • Never underestimate COM/WinRT interop. Even in scenarios that might seem straightforward at first, it’s very likely that there’s some less apparent complexity that you will have to deal with once you start diving deep into the implementation. Always keep an open mind and assume that you might very well end up discovering and learning something new that you weren’t aware before.
  • Especially due to the additional hoops we had to go through to get the marshalling working on .NET Native, it’s clear how things could’ve been easier for us if .NET Native had just exposed its internal helpers publicly (eg. that McgMarshal.ComInterfaceToObject API).
    • We think this is one of the things that CsWinRT is doing right, where it’s explicitly leaving most of its internal lowlevel helpers public (of course, in some specific, less visible namespace), to give developers wanting to implement things manually ready to use tools to make their lives easier.
    • We think this is a great API design: if you’re building a library that’s dealing with some kind of native interop, it might be a good idea to still expose some basic building blocks that are internally used, even if most consumers will likely not need them. Chances are that, like in this case, there will be few that will be able to save a lot of time by having them readily available with no need for them to reinvent the wheel.
    • Of course, there is also the argument that the more public API surface you have, the greater the maintenance effort and the harder it is to then make changes. But, for small interop helpers that are very narrow scoped (eg. ExceptionHelpers.SetErrorInfo in CsWinRT), this is often just a net positive.
  • Some of the additional complexity in this post is of course also due to the unique fact that UWP apps using .NET run on two different runtimes (.NETCore.Uwp in Debug, and .NET Native in Release), which is generally transparent to developers but can become apparent and cause some friction in niche cases such as this one. For instance, this would not have been an issue in a WinUI 3 app, as the same runtime would’ve been used in all cases (hence, the code would’ve either always worked, or not at all, but with a consistent behavior regardless of build configuration).
  • In general though, small issues like this could arise in all cases where different runtimes are used to run the same code, especially if parts of that code are (deliberately or not) relying on some runtime-specific behavior. For this reason, extensive testing should also be done whenever the same application or library is deployed or executed using eg. not just CoreCLR, but also Mono, NativeAOT, or Unity.
  • If you’re wondering why the Microsoft Store is a UWP application, it is because it is using some features that are exclusive to UWP applications, as well as targeting not just Windows Desktop, but HoloLens and Surface Hub as well.

Lastly, we’re also working with the Windows Package Manager team to open source these APIs we designed for the Store. As we discussed, instantiating the various objects to interact with Windows Package Manager is particularly tricky to do on C#, and requires significant more work than on C++. Because of this, we’re exploring exposing these same APIs we’re now using in the Microsoft Store through winget-cli’s COM interop NuGet package, to enable all .NET developers to easily interact with Windows Package Manager without having to do all this extra work on their own! 🙌

Happy coding! 💻

6 comments

Discussion is closed. Login to edit/delete existing comments.

  • Paulo Pinto 2

    Very interesting deep dive.

    However it is also an example of the two parallel universes that keep existing at Microsoft, .NET and COM/UWP, where we have to jump trough lots of hurdles when Windows teams only release libraries in COM.

    Naturally employees have an access channel where these issues can eventually be sorted out, whereas we on the outside have to just deal with it in mixed source code bases of C# and C++ wrappers, move hope and hope it doesn’t break in upcoming OS releases.

    On a positive note, maybe this can help to have better out of the box experience for dealing with COM from .NET, as owning “.NET and COM: The Complete Interoperability Guide” is not enough.

  • Huo Yaoyuan 0

    .NET Native is really painful to use, especially for interop.
    Hope you can bring WinUI 3 to feature complete soon so that we can move to modern .NET. The system built-in apps are always the guidance for us.

  • Игорь Баклыков 0

    Well, how bad should the implementation be that C++ code builds a lot more than C# 🤔

    • Clem Ca 0

      We’re talking about compilation time, not run time. C++ code might run faster than C# code, but C# tends to compile many times faster, which is why it’s easier to iterate with.

  • Mystery Man 1

    I wish the functionality of the Store and WinGet were available through PowerShell. I like WinGet but it is a legacy CLI app. Its output is text, not a .NET object.

  • Rasmussen, Eric 1

    This is a reason I wish the Windows team chose not to standardize the API on legacy technology like COM. I was writing COM and COM+ components back in the 90’s, I’ve been waiting for a .NET-based Windows shell for the past 20 years. 😁

Feedback usabilla icon