“.NET Native” 和编译

面向 .NET Framework 的 Windows 桌面应用程序以特定编程语言编写,并编译为中间语言(IL)。 在运行时,实时 (JIT) 编译器负责将 IL 编译为本地计算机的本机代码,然后再首次执行方法。 相比之下,.NET Native 工具链在编译时将源代码转换为本机代码。 本文将 .NET Native 与其他可用于 .NET Framework 应用的编译技术进行比较,并提供有关 .NET Native 如何生成本机代码的实用概述,可帮助你了解使用 .NET Native 编译的代码中发生的异常为何不会出现在 JIT 编译的代码中。

生成本机二进制文件

面向 .NET Framework 且未使用 .NET Native 工具链编译的应用程序由应用程序程序集组成,其中包括:

  • 元数据,描述程序集、其依赖项、包含的类型及其成员。 元数据用于反射和后期绑定访问,在某些情况下也由编译器和生成工具使用。

  • 实现代码。 这包括中间语言(IL)指令码。 在运行时,即时编译(JIT)编译器将其转换为目标平台的本机代码。

除了主应用程序程序集之外,应用还要求存在以下内容:

  • 应用所需的任何其他类库或第三方程序集。 这些程序集同样包括描述程序集、其类型及其成员的元数据,还有用于实现所有类型成员的 IL。

  • .NET Framework 类库。 这是随 .NET Framework 安装在本地系统上的程序集集合。 .NET Framework 类库中的程序集包含一组完整的元数据和实现代码。

  • 公共语言运行时。 这是动态链接库的集合,用于执行程序集加载、内存管理和垃圾回收、异常处理、实时编译、远程处理和互操作等服务。 与类库一样,运行时作为 .NET Framework 安装的一部分安装在本地系统上。

请注意,应用程序要成功执行,必须同时存在整个公共语言运行时以及应用程序特定程序集、第三方程序集和系统程序集中的所有类型的元数据和 IL。

即时编译

.NET Native 工具链的输入是由 C# 或 Visual Basic 编译器生成的 UWP 应用。 换句话说,当语言编译器完成 UWP 应用的编译后,.NET Native 工具链将开始执行。

小窍门

由于 .NET Native 的输入是 IL 和写入托管程序集的元数据,因此你仍然可以使用预生成或生成后事件或通过修改 MSBuild 项目文件来执行自定义代码生成或其他自定义操作。

不支持能够修改 IL 并因此阻止 .NET 工具链分析应用程序 IL 的工具类型。 混淆器是此类工具中最引人注目的。

在将应用从 IL 转换为本机代码的过程中,.NET Native 工具链执行如下操作:

  • 对于某些代码路径,它将依赖反射和元数据的代码替换为静态本地代码。 例如,如果值类型不重写 ValueType.Equals 方法,则默认的相等性测试使用反射来获取表示值类型的字段的 FieldInfo 对象,然后比较这两个实例的字段值。 编译为本机代码时,.NET Native 工具链会用对字段值的静态比较来替换反射代码和元数据。

  • 如果可能,它会尝试消除所有元数据。

  • 它仅在最终的应用程序集中包含实际被应用程序调用的实现代码。 这特别影响第三方库和 .NET Framework 类库中的代码。 因此,应用程序不再依赖于第三方库或完整的 .NET Framework 类库;相反,第三方和 .NET Framework 类库中的代码现在是应用的本地代码。

  • 它将完整的 CLR 替换为主要包含垃圾回收器的重构运行时。 重构运行时位于应用本地的名为 mrt100_app.dll 的库中,并且大小仅为几百 KB。 这是可能的,因为静态链接免去了对公共语言运行时所执行的许多服务的需求。

    注释

    .NET Native 使用与标准公共语言运行时相同的垃圾回收器。 在 .NET 本机垃圾回收器中,默认启用后台垃圾回收。 有关垃圾回收的详细信息,请参阅 垃圾回收的基础知识。

重要

.NET Native 将整个应用程序编译为本机应用程序。 它不允许将包含类库的单个程序集编译为本机代码,以便独立于托管代码调用它。

.NET Native 工具链生成的应用将写入项目目录的“调试”或“发布”目录中名为 ilc.out 的目录。 它包含以下文件:

  • <appName>.exe,一个存根可执行文件,它仅仅是将控制权传递到 >.dll中的一个特殊的 导出。

  • <appName>.dll,一个 Windows 动态链接库,其中包含所有应用程序代码,以及来自 .NET Framework 类库的代码以及依赖的任何第三方库的代码。 它还包含支持代码,例如与 Windows 互操作和在应用中序列化对象所需的代码。

  • mrt100_app.dll,一个经过重构的运行时,提供例如垃圾回收等运行时服务。

所有依赖项均由应用的 APPX 清单捕获。 除了应用程序 exe、dll 和 mrt100_app.dll(直接捆绑在 appx 包中),还包括另外两个文件:

  • msvcr140_app.dll,mrt100_app.dll使用的 C 运行时 (CRT) 库。 它被包中的框架引用所包含。

  • mrt100.dll。 此库包含可提高 mrt100_app.dll性能的函数,尽管其缺席不会阻止 mrt100_app.dll 正常运行。 如果存在,则从本地计算机上的 system32 目录加载它。

由于 .NET Native 工具仅当知道应用实际调用该代码时,才会将实现代码链接到应用中,因此以下方案中所需的元数据或实现代码可能不包含在应用中:

  • 反思。

  • 动态或后期绑定调用。

  • 序列化和反序列化。

  • COM 互操作。

如果运行时缺少必要的元数据或实现代码,.NET Native 运行时将引发异常。 可以阻止这些异常,并确保 .NET Native 工具链包含所需的元数据和实现代码,方法是使用 运行时指令文件,该文件指定其元数据或实现代码必须在运行时可用的程序元素,并向其分配运行时策略。 以下是通过 .NET Native 工具链编译的 UWP 项目中所添加的默认运行时指令文件:

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  <Application>
    <Assembly Name="*Application*" Dynamic="Required All" />
  </Application>
</Directives>

这样就可以在应用包中的所有程序集中启用所有类型及其所有成员,以便进行反射和动态调用。 但是,它不会在 .NET Framework 类库程序集中启用类型的反射机制或动态激活功能。 在许多情况下,这已经足够了。

另请参阅