【UWP】在 UWP 中使用 Windows App SDK

魔法薇ㄦ的菜园 / 2024-10-23 / 原文

众所周知,WAS (Windows App SDK,俗称 WinUI3)在刚开始是支持 UWP 的,甚至最早只支持 UWP,但是微软在正式版发布前删除了对 UWP 的支持,不过真的删除了吗?初生之鸟在2023年10月发现在 VS 调试下无视报错继续运行可以正常在 UWP 加载 WAS。随着 WAS 的开源,WAS 阻止在 UWP 上运行的原因也被找到,至此大家终于找到在 UWP 上使用 WAS 的方法了。

WAS 阻止在 UWP 上运行的方法很简单,就是检查注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WinUI\Xaml\EnableUWPWindow是否为00000001,如果不是就直接报错。

Window_Partial.cpp#L80-L114
// ----------------------------------------------------------------------
//                               IWindow
// ----------------------------------------------------------------------
Window::Window()
{
    // The first window created internally by DXamlCore _must_ be a UWP Window.  DXamlCore
    // requires and controls the lifetime of a hidden UWP Microsoft.UI.Xaml.Window.
    // note that this Window instance will be the 'real' window for UWP instances, but
    // serves as a dummy for all other instances. dummy behavior is deprecated and being removed.
    auto dxamlCore = DXamlCore::GetCurrent();
    Window* window = dxamlCore->GetDummyWindowNoRef();

    if (!window)
    {
        // Do a runtime check to see if UWP should be enabled
        static auto runtimeEnabledFeatureDetector = RuntimeFeatureBehavior::GetRuntimeEnabledFeatureDetector();
        auto UWPWindowEnabled = runtimeEnabledFeatureDetector->IsFeatureEnabled(RuntimeEnabledFeature::EnableUWPWindow);

        // WinUI UWP
        if (!UWPWindowEnabled && DXamlCore::GetCurrent()->GetHandle()->GetInitializationType() != InitializationType::IslandsOnly)
        {
            ::RoOriginateError(
                E_NOT_SUPPORTED,
                wrl_wrappers::HStringReference(
                L"WinUI: Error creating an UWP Window. Creating an UWP window is not allowed."
                ).Get());
            XAML_FAIL_FAST();
        }
        m_spWindowImpl = std::make_shared<UWPWindowImpl>(this);
    }
    else
    {
        m_spWindowImpl = std::make_shared<DesktopWindowImpl>(this);
    }
}
Window_Partial.cpp#L80-L114
{ L"EnableUWPWindow", RuntimeEnabledFeature::EnableUWPWindow, false, 0, 0 }

所以我们只需要修改注册表就行了。

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WinUI\Xaml]
"EnableUWPWindow"=dword:00000001

但是到处修改注册表并不是一个好主意,于是初生之鸟便提出利用Detours来劫持读取注册表的方法:HookCoreAppWinUI。

我们将其翻译成 C#,再加一些小修改,便能得出如下内容:

#r "nuget:Detours.Win32Metadata"
#r "nuget:Microsoft.Windows.CsWin32"

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Registry;
using Detours = Microsoft.Detours.PInvoke;

/// <summary>
/// Represents a hook for getting the value of the <c>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WinUI\Xaml\EnableUWPWindow</c> registry key always returning <see langword="00000001"/>.
/// </summary>
public partial class HookRegistry : IDisposable
{
    /// <summary>
    /// The value that indicates whether the class has been disposed.
    /// </summary>
    private bool disposed;

    /// <summary>
    /// The reference count for the hook.
    /// </summary>
    private static int refCount;

    /// <summary>
    /// The dictionary that maps the <see cref="HKEY"/> to a value that indicates whether the key is a real key.
    /// </summary>
    private static readonly Dictionary<HKEY, bool> xamlKeyMap = [];

    /// <summary>
    /// The object used to synchronize access to the <see cref="xamlKeyMap"/> dictionary.
    /// </summary>
    private static readonly object locker = new();

    /// <remarks>The original <see cref="PInvoke.RegOpenKeyEx(HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*)"/> function.</remarks>
    /// <inheritdoc cref="PInvoke.RegOpenKeyEx(HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*)"/>
    private static unsafe delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*, WIN32_ERROR> RegOpenKeyExW;

    /// <remarks>The original <see cref="PInvoke.RegCloseKey(HKEY)"/> function.</remarks>
    /// <inheritdoc cref="PInvoke.RegCloseKey(HKEY)"/>
    private static unsafe delegate* unmanaged[Stdcall]<HKEY, WIN32_ERROR> RegCloseKey;

    /// <remarks>The original <see cref="PInvoke.RegQueryValueEx(HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*)"/> function.</remarks>
    /// <inheritdoc cref="PInvoke.RegQueryValueEx(HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*)"/>
    private static unsafe delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*, WIN32_ERROR> RegQueryValueExW;

    /// <summary>
    /// Initializes a new instance of the <see cref="HookRegistry"/> class.
    /// </summary>
    public HookRegistry()
    {
        refCount++;
        StartHook();
    }

    /// <summary>
    /// Finalizes this instance of the <see cref="HookRegistry"/> class.
    /// </summary>
    ~HookRegistry()
    {
        Dispose();
    }

    /// <summary>
    /// Gets the value that indicates whether the hook is active.
    /// </summary>
    public static bool IsHooked { get; private set; }

    /// <summary>
    /// Starts the hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
    /// </summary>
    private static unsafe void StartHook()
    {
        if (!IsHooked)
        {
            using FreeLibrarySafeHandle library = PInvoke.GetModuleHandle("ADVAPI32.dll");
            if (!library.IsInvalid
                && NativeLibrary.TryGetExport(library.DangerousGetHandle(), "RegOpenKeyExW", out nint regOpenKeyExW)
                && NativeLibrary.TryGetExport(library.DangerousGetHandle(), nameof(PInvoke.RegCloseKey), out nint regCloseKey)
                && NativeLibrary.TryGetExport(library.DangerousGetHandle(), "RegQueryValueExW", out nint regQueryValueExW))
            {
                void* regOpenKeyExWPtr = (void*)regOpenKeyExW;
                void* regCloseKeyPtr = (void*)regCloseKey;
                void* regQueryValueExWPtr = (void*)regQueryValueExW;

                delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*, WIN32_ERROR> overrideRegOpenKeyExW = &OverrideRegOpenKeyExW;
                delegate* unmanaged[Stdcall]<HKEY, WIN32_ERROR> overrideRegCloseKey = &OverrideRegCloseKey;
                delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*, WIN32_ERROR> overrideRegQueryValueExW = &OverrideRegQueryValueExW;

                _ = Detours.DetourRestoreAfterWith();

                _ = Detours.DetourTransactionBegin();
                _ = Detours.DetourUpdateThread(PInvoke.GetCurrentThread());
                _ = Detours.DetourAttach(ref regOpenKeyExWPtr, overrideRegOpenKeyExW);
                _ = Detours.DetourAttach(ref regCloseKeyPtr, overrideRegCloseKey);
                _ = Detours.DetourAttach(ref regQueryValueExWPtr, overrideRegQueryValueExW);
                _ = Detours.DetourTransactionCommit();

                RegOpenKeyExW = (delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*, WIN32_ERROR>)regOpenKeyExWPtr;
                RegCloseKey = (delegate* unmanaged[Stdcall]<HKEY, WIN32_ERROR>)regCloseKeyPtr;
                RegQueryValueExW = (delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*, WIN32_ERROR>)regQueryValueExWPtr;

                IsHooked = true;
            }
        }
    }

    /// <summary>
    /// Ends the hook for the <see cref="PInvoke.AppPolicyGetWindowingModel(HANDLE, AppPolicyWindowingModel*)"/> function.
    /// </summary>
    public static unsafe void EndHook()
    {
        if (--refCount == 0 && IsHooked)
        {
            void* regOpenKeyExWPtr = RegOpenKeyExW;
            void* regCloseKeyPtr = RegCloseKey;
            void* regQueryValueExWPtr = RegQueryValueExW;

            delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*, WIN32_ERROR> overrideRegOpenKeyExW = &OverrideRegOpenKeyExW;
            delegate* unmanaged[Stdcall]<HKEY, WIN32_ERROR> overrideRegCloseKey = &OverrideRegCloseKey;
            delegate* unmanaged[Stdcall]<HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*, WIN32_ERROR> overrideRegQueryValueExW = &OverrideRegQueryValueExW;

            _ = Detours.DetourTransactionBegin();
            _ = Detours.DetourUpdateThread(PInvoke.GetCurrentThread());
            _ = Detours.DetourDetach(&regOpenKeyExWPtr, overrideRegOpenKeyExW);
            _ = Detours.DetourDetach(&regCloseKeyPtr, overrideRegCloseKey);
            _ = Detours.DetourDetach(&regQueryValueExWPtr, overrideRegQueryValueExW);
            _ = Detours.DetourTransactionCommit();

            RegOpenKeyExW = null;
            RegCloseKey = null;
            RegQueryValueExW = null;

            IsHooked = false;
        }
    }

    /// <remarks>The overridden <see cref="PInvoke.RegOpenKeyEx(HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*)"/> function.</remarks>
    /// <inheritdoc cref="PInvoke.RegOpenKeyEx(HKEY, PCWSTR, uint, REG_SAM_FLAGS, HKEY*)"/>
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
    private static unsafe WIN32_ERROR OverrideRegOpenKeyExW(HKEY hKey, PCWSTR lpSubKey, uint ulOptions, REG_SAM_FLAGS samDesired, HKEY* phkResult)
    {
        WIN32_ERROR result = RegOpenKeyExW(hKey, lpSubKey, ulOptions, samDesired, phkResult);
        if (hKey == HKEY.HKEY_LOCAL_MACHINE && lpSubKey.ToString().Equals(@"Software\Microsoft\WinUI\Xaml", StringComparison.OrdinalIgnoreCase))
        {
            if (result == WIN32_ERROR.ERROR_FILE_NOT_FOUND)
            {
                HKEY key = new(HANDLE.INVALID_HANDLE_VALUE);
                xamlKeyMap[key] = false;
                *phkResult = key;
                result = WIN32_ERROR.ERROR_SUCCESS;
            }
            else if (result == WIN32_ERROR.ERROR_SUCCESS)
            {
                xamlKeyMap[*phkResult] = true;
            }
        }
        return result;
    }

    /// <remarks>The overridden <see cref="PInvoke.RegCloseKey(HKEY)"/> function.</remarks>
    /// <inheritdoc cref="PInvoke.RegCloseKey(HKEY)"/>
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
    private static unsafe WIN32_ERROR OverrideRegCloseKey(HKEY hKey)
    {
        bool isXamlKey;
        lock (locker)
        {
            if (isXamlKey = xamlKeyMap.TryGetValue(hKey, out bool isRealKey))
            {
                xamlKeyMap.Remove(hKey);
            }
            return isXamlKey
                ? isRealKey
                    ? RegCloseKey(hKey) // real key
                    : WIN32_ERROR.ERROR_SUCCESS // simulated key
                : hKey == HANDLE.INVALID_HANDLE_VALUE
                    ? WIN32_ERROR.ERROR_INVALID_HANDLE
                    : RegCloseKey(hKey);
        }
    }

    /// <remarks>The overridden <see cref="PInvoke.RegQueryValueEx(HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*)"/> function.</remarks>
    /// <inheritdoc cref="PInvoke.RegQueryValueEx(HKEY, PCWSTR, uint*, REG_VALUE_TYPE*, byte*, uint*)"/>
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
    private static unsafe WIN32_ERROR OverrideRegQueryValueExW(HKEY hKey, PCWSTR lpValueName, [Optional] uint* lpReserved, [Optional] REG_VALUE_TYPE* lpType, [Optional] byte* lpData, [Optional] uint* lpcbData)
    {
        if (lpValueName.Value != default && lpValueName.ToString().Equals("EnableUWPWindow", StringComparison.OrdinalIgnoreCase))
        {
            lock (locker)
            {
                if (xamlKeyMap.TryGetValue(hKey, out bool isRealKey))
                {
                    WIN32_ERROR result;
                    if (isRealKey)
                    {
                        // real key
                        result = RegQueryValueExW(hKey, lpValueName, lpReserved, lpType, lpData, lpcbData);
                        if (result == WIN32_ERROR.ERROR_SUCCESS && lpData != default)
                        {
                            *lpData = 1;
                        }
                        else if (result == WIN32_ERROR.ERROR_FILE_NOT_FOUND)
                        {
                            if (lpData == default && lpcbData != default)
                            {
                                *lpcbData = sizeof(int);
                                result = WIN32_ERROR.ERROR_SUCCESS;
                            }
                            else if (lpData != default && lpcbData != default)
                            {
                                if (*lpcbData >= sizeof(int))
                                {
                                    *lpData = 1;
                                    result = WIN32_ERROR.ERROR_SUCCESS;
                                }
                                else
                                {
                                    result = WIN32_ERROR.ERROR_MORE_DATA;
                                }
                            }
                        }
                    }
                    else
                    {
                        // simulated key
                        result = WIN32_ERROR.ERROR_FILE_NOT_FOUND;
                        if (lpData == default && lpcbData != default)
                        {
                            *lpcbData = sizeof(int);
                            result = WIN32_ERROR.ERROR_SUCCESS;
                        }
                        else if (lpData != default && lpcbData != default)
                        {
                            if (*lpcbData >= sizeof(int))
                            {
                                *lpData = 1;
                                result = WIN32_ERROR.ERROR_SUCCESS;
                            }
                            else
                            {
                                result = WIN32_ERROR.ERROR_MORE_DATA;
                            }
                        }
                    }
                    return result;
                }
            }
        }
        return RegQueryValueExW(hKey, lpValueName, lpReserved, lpType, lpData, lpcbData);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (!disposed && IsHooked)
        {
            EndHook();
        }
        GC.SuppressFinalize(this);
        disposed = true;
    }
}

随后我们只需要在入口点创建App时进行劫持就行了。

private static bool IsSupportCoreWindow
{
    get
    {
        try
        {
            RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\WinUI\Xaml");
            return registryKey?.GetValue("EnableUWPWindow") is > 0;
        }
        catch
        {
            return false;
        }
    }
}

private static void Main()
{
    ComWrappersSupport.InitializeComWrappers();
    HookRegistry hookRegistry = null;
    try
    {
        if (!IsSupportCoreWindow)
        {
            hookRegistry = new HookRegistry();
        }
        XamlCheckProcessRequirements();
        Application.Start(p =>
        {
            DispatcherQueueSynchronizationContext context = new(DispatcherQueue.GetForCurrentThread());
            SynchronizationContext.SetSynchronizationContext(context);
            _ = new App();
        });
    }
    finally
    {
        hookRegistry?.Dispose();
    }
}

当然想要自定义入口函数,我们需要在csproj加上定义。

<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>

同时还要记得在清单中明确入口点。

<?xml version="1.0" encoding="utf-8"?>
<Package ...>
  ...
  <Applications>
    <Application ...
      EntryPoint="明确的入口点">
      ...
    </Application>
  </Applications>
  ...
</Package>

随后我们就可以正常的使用 UWP/WAS 了。

最后附上示例应用:https://github.com/wherewhere/CoreAppUWP/tree/muxc