Package Dependency
Windows MSIX Package 目前来看主要有三种:
- Application Package,使用
<Application Executable="Path" />
定义入口程序。 - Framework Package,设置
<Framework>true</Framework>
。- WinUI2、WinAppSDK、DirectX、VCLib 等。
- Resource Package,包含字符串、图像等资源。
- 最常见的用例是按需下载的语言包。
第一个容易理解,然而后两者怎么起作用初看会令人困惑。实际上这是基于 MSIX Package Dependency 机制的。我们下面使用一个例子来解释它是怎么工作的。
AppxManifest.xml 中可以设置 <Dependencies />
,例如 WinUI3Gallery 应用拥有以下依赖:
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.19041.0" />
<PackageDependency Name="Microsoft.WindowsAppRuntime.1.3" MinVersion="3000.820.152.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
<PackageDependency Name="Microsoft.VCLibs.140.00" MinVersion="14.0.30704.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
<PackageDependency Name="Microsoft.VCLibs.140.00.UWPDesktop" MinVersion="14.0.30704.0" Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" />
</Dependencies>
可以看到,除了第一个是指定操作系统版本之外,后三个都是指定框架包依赖。但其实它们之间也有区别。
Microsoft.WindowsAppRuntime.1.3
是 WinAppSDK 的框架包,它是一个 WinRT API 框架,用户程序通过 ActivationFactory 获取其接口。但是默认 WinRT 是按 COM 激活,也即本质是基于注册表激活的,怎么基于包激活呢?其 AppxManifest.xml 中含有<Extension Category="windows.activatableClass.inProcessServer" />
,表示可激活的 WinRT DLL 与 Class 的从属关系。<Extension Category="windows.activatableClass.proxyStub" />
,表示一个 OutOfProc WinRT Server 的 RPC ProxyStub。可以想象到,这个 Server 肯定也是某个包里 XML 注册的,实际上语法就是<com:Extension Category="windows.comServer"><com:ComServer /></com:Extension>
。- 使用 XML 定义好这些之后,至少信息已经足以用于 WinRT Activation 了,并且通过包的元信息提供给 Windows 了,Windows 该做的就是按照包依赖反向注册,在激活时加以考虑,找到全部可能的激活路径。
- 该机制一般被称为 PackagedCOM,详情可以看这篇文章。
Microsoft.VCLibs.140.00
和Microsoft.VCLibs.140.00.UWPDesktop
都是 VC 运行库,一个给 UWP 一个给桌面用,其下包目录下全部都是 dll 。- 可以想见的是,在调用
LoadLibrary
时,搜索器会考虑 PackageDependency,从而使用到这个运行库。
- 可以想见的是,在调用
有没有觉得这个机制很眼熟?没错,DLL、COM Server,介入 DLL Loader 和 COM 激活机制,Per-application 的 Manifest,这不就是 WinSxS Assembly 吗?这不就是 Isolation 吗?倒也不用见怪,现代包管理都是殊途同归的,WinSxS 思想确实超前 NixOS 十年,只不过说不定人们都没发现某个方法其实是一大类问题的万金油。
不过 PackageDependency 并非只能做 Assembly 已经做到的事,它实际上在以下全部方面都起作用:
- DLL Loader - 在 Dynamic-Link Library Search Order 中处于 CWD 之前,是旧的所有方法都找不到时才会考虑的选项。
- Common Language Runtime - .Net CLR 会直接在包依赖中寻找程序集加载,本质是 DLL Loader 的 .Net 版本,具体 Search Order 未知,应该也是最后的 Fallback 。
- COM - 上文都说了有 PackagedCOM 了,COM 的激活当然也是支持的。
- Windows Runtime - 上文也提到了,是通过 XML 定义激活元信息配置的。
- 这里可以类比一下 CLR,因为 WinRT 本质就是 Windows 上的原生 CLR,连元信息(.winmd) 都用的是同一套(好像是经过 ECMA 标准化的)格式,可以用 ildasm.exe 查看。
- 为什么 CLR 不需要 XML 配置呢?我猜测是因为 .Net DLL 里都包含了 .Net Assembly Metadata,也就是一个 .Net DLL 就等价于一个 Self-contained Package,其 Assembly Identity 是全局唯一的,可以用于直接识别。
- 而 .winmd 和 IDL 可以互相转化,它没有 Assembly Identity (用 ildasm 可以看到基本是个空定义,我猜应该就是 placeholder ),而只有 Interface Definition。winmd 还是可组合的,一个包里可以有多个 winmd,可以说这些 winmd 分享了母包的 Package Identity 用于激活。
- 那为什么 .winmd 不能像 CLR 一样,解析 metadata 来获得信息,非要我们手动配置 ActivatableClass?这个我其实……圆不回来了。我觉得大概率是 CLR 当然可以轻松解析 .Net DLL 内的信息,但是 WinRT Activation Factory 其实是二进制接口 (
RoGetActivationFactory
) + 字符串激活 (HKLM\SOFTWARE\Microsoft\WindowsRuntime
),为了激活性能的考虑,或者为了不在系统级 DLL 中引入一个有一些体积的 winmd parser 等,都有可能。 - 文档里还提到了 WinRT Type Resolution 的流程就是给 Windows.* 开个洞,然后加上 PackageDependency,虽然没有强调,但他的意思是其它的一概不会考虑。这个忽略了注册表,我也不知道为什么会这么说。
- MRT / MRTCore - UWP 和 WinAppSDK 的两套资源管理系统,命名空间分别为
Windows/Microsoft.Windows.ApplicationModel.Resources
,它们在解析时会把 PackageDependencies 划为不同的 ResourceMap 来访问,这也是 ResourcePackage 可以工作的原因。 - ms-appx URI 协议 - ms-appx URIs 的 authority 部分可以填一个 Package Identity,如果为空就是 Package.Current,如果非空则会从 PackageDependencies 中解析。
除此之外,Dependency 还参与 Package Deployment,或者叫 Servicing(安装、更新与卸载)。例如假如一个 Framework 正在被某些应用依赖,它就不能立刻被更新,应该延迟到重启时等时候。涉及到版本以及卸载等情况的话就更复杂,这里必须要有一个严格并且鲁棒的引用计数机制,并且最好有保底机制存在。
这样解释下来应该可以看出 Package Dependency 的强大之处了,它是一个现代版的、和中上层抽象(WinRT、AppModel)紧密协作的 Isolation 机制。微软做这种东西已经得心应手(Windows Componentization Platform Servicing),所以这个现代版本还是相当 polish 的。
困境
产品矩阵
Permission | Packaged | Unpackaged |
---|---|---|
App Container | UWP | Non sense |
Desktop Non-elevation | MSIX | Win32 Native |
Desktop Elevation | MSIX w/o PackagedCOM | Win32 Native |
由于众所周知的原因,在 Windows 上大部分应用处于并将长期处于 Unpackaged 状态,当然也就和 PackageDependency 无缘。这样一来微软想要推广自己的基于 Dependency 的 Framework Package 时就没法覆盖这部分开发者。
Dynamic Dependency
为了让这些 Unpackaged 应用也能享受到 Package Dependency 的福利(这是主要原因),就需要提供一个方法给没有 Manifest 的应用告诉操作系统自己有什么 Dependency 。显然这个方法是运行时的,与 XML 描述的 Static Dependency 不同,所以它被称作 Dynamic Dependency。为了实现这一点,微软分了三步走
- 推出 MSIX 支持桌面应用打包。
- Windows DynamicDependency API (Win11 Only),为 Unpackaged 应用做原生支持。
- Windows App SDK DynamicDependency API (Down to Win10 1809),类似于 androidx 或者 WinUI 3,用模拟的方式在用户态将最新平台功能下放到旧平台。
其中,1 不仅作为反向解决问题(消灭提出问题的应用)的办法,也作为 3 的基础。
实际上你可能要问了,Dependency 说到底也就是个存在内存中的图而已,修改一下到底有什么难的?其实这就是 2 的玩法,它修改了 Package Deployment 机制,让其对 Dynamic Dependency 有感知。具体做了什么?原本由于 Dependency 都是静态的,所以许多东西会被 Cache 下来,现在这些东西全部都要么废除 Cache,要么监听 Cache Invalidation(通过 Dependency Graph Revision Id,每次修改都会自增,使得 Cache 知道自己是旧的)。
下面重点讲讲 3 的玩法。DD 有两部分任务:
- 保证 Dependency Effect,也即上面说的 DLL Loader 之类的。
- 这些 Effect 也是库,Windows 喜欢用 dll 实现系统功能,本质就是一个用户态的库。具体到这里来说,对于 Dependency 的读取目前全部来自于两个 API:
GetCurrentPackageInfo
和GetCurrentPackageInfo2
。同时,这些 Effect 还会用其他的一些 MSIX AppModel API 查询当前包的信息。 - 如果我们 Hook 这些 MSIX AppModel API,假传圣旨,可以轻松假装当前进程是一个 Packaged 应用,并掺进我们的私货依赖,完全不用修改这些库的代码就可以让效果生效——
- 是这样吗?上文我们说了以前静态的东西现在动态,很可能以前的 Cache 现在会失效,这个问题还是需要手动解决。
- 很多机制并不叫做 Cache,但是本质是 Cache 。例如 DLL Search Order 会在进程创建时获取 PackageDependency 并计算一次 Order,此后再也不更新,这不是 Cache 胜似 Cache 。
- 对于这种,只能从 Effect 本身下手,例如使用
AddDllDirectory
API 手动修改。是个脏活,但也没办法,Hook 都出来了也不用考虑脏不脏的了。
- 这些 Effect 也是库,Windows 喜欢用 dll 实现系统功能,本质就是一个用户态的库。具体到这里来说,对于 Dependency 的读取目前全部来自于两个 API:
- 保证 Deployment Awareness,也即上面说的判断能否更新、一个旧版本的包能否被删除等场景。
- 制造一个依赖框架包的假包 DDLM,使用者在自己应用启动时启动它。
- 这也是 WinAppSDK 自己的做法,这个包由 WinAppSDK 主动提供。
- 可以预见的是,Unpackged 应用在不主动制造假包,并且框架方也没有提供 DDLM,并且版本低于 Windows 11 的时候,Package Dependency 的 Deployment Awareness 没有办法被保证。