该页面可以包含自动翻译的文本。
如何在 .NET 中开发 Native AOT 应用程序
.NET 8 全面支持提前编译,也称为 Native AOT。此类应用程序通常比托管解决方案更快,消耗的内存更少。
.NET AOT 发布可生成精简的、自包含的应用程序。此类应用程序:
- 可以在未安装 .NET 运行时的机器上运行。
- 针对特定的运行时环境,例如 Windows x64。
- 不包含未使用的代码。
我们开发了用于 PDF 处理的 Docotic.Pdf 库。AOT 兼容性是 2024 年最受欢迎的功能请求之一。本文介绍了我们实现 AOT 兼容性的历程。您将学习如何在 .NET 项目中查找和修复 AOT 问题。
.NET AOT 发布基础知识
要发布 Native AOT 应用程序,请将以下属性添加到您的项目文件中:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
您可能还需要安装一些 先决条件。然后,您可以从
Visual Studio 发布您的应用程序。或者,您可以使用 dotnet publish
命令从命令行发布项目:
dotnet publish -r linux-x64 -c Release
.NET 库的 AOT 兼容性
Native AOT 发布功能在 .NET 7 和 .NET 8 中可用。在 .NET 7 中,您只能发布控制台应用程序。在 .NET 8 中,您还可以发布 ASP.NET Core 应用程序。
但是 Docotic.Pdf 核心库面向 .NET Standard 2.0。我们可以让它对 AOT 友好吗?当然。
您仍然可以在针对 .NET Standard、.NET 5 或 .NET 6 的 .NET 库中查找和修复 AOT 兼容性问题。Native AOT 应用程序可以依赖该库。
查找 AOT 问题
我们关于 AOT 兼容性的第一个支持请求听起来像这样:
我们正在考虑使用编译为 WebAssembly 的 Docotic.Pdf。当我们将其编译为 AOT 时,它会生成修剪警告:
警告 IL2104:程序集“BitMiracle.Docotic.Pdf”产生了修剪警告。有关更多信息,请参阅 https://aka.ms/dotnet-illink/libraries
信息量不大。让我们重现该问题并获取有关这些修剪警告的更多详细信息。
获取发布警告
发布测试应用程序是查找 .NET AOT 问题的最灵活方法。您需要:
- 使用
PublishAot = true
创建一个 .NET 8 控制台项目 - 添加对项目的引用并使用其某些类型
以下是我们用于 Docotic.Pdf 的 csproj 配置:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Docotic\Docotic.csproj" />
</ItemGroup>
</Project>
TrimmerSingleWarn = false
允许您获取有关警告的详细信息。否则,您只会收到“程序集‘X’产生了修剪警告”消息。
以下是 Program.cs
中的 C# 代码:
using BitMiracle.Docotic.Pdf;
using var pdf = new PdfDocument();
添加一些代码以强制加载正在检查的程序集。无需覆盖项目的所有功能即可获得发布警告。使用单一类型就足够了。但您需要编写更多代码来捕获运行时错误。
该项目的 dotnet publish -r win-x64 -c Release
命令返回了修剪和 AOT 警告。以下是列表的简短版本:
AOT 分析警告 IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): 使用具有“RequiresDynamicCodeAttribute”的成员“System.Enum.GetValues(Type)”可能会在 AOT 编译时破坏功能。
修剪分析警告 IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): 使用具有“RequiresUnreferencedCodeAttribute”的成员“System.Linq.Expressions.Expression.Property(Expression,String)”可能会在修剪应用程序代码时破坏功能。
修剪分析警告 IL2057: LogProviders.LogProviderBase.FindType(String,String[]): 传递给方法“System.Type.GetType(String)”的参数“typeName”的值无法识别。
修剪分析警告 IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 在调用“System.Type.GetProperty(String)”时,“this”参数不满足“DynamicallyAccessedMemberTypes.PublicProperties”。方法“LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)”的参数“loggerType”没有匹配的注释。源值必须至少声明与其分配到的目标位置上声明的要求相同的要求。
修剪分析警告 IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 在调用“System.Type.GetProperty(String)”时,“this”参数不满足“DynamicallyAccessedMemberTypes.PublicProperties”。方法“LogProviders.LogProviderBase.FindType(String,String)”的返回值没有匹配的注释。源值必须至少声明与其分配到的目标位置上声明的要求相同的要求。
我们找到了根本原因。在修复问题之前,让我们检查一下查找 AOT 兼容性问题的替代选项。
使用 AOT 兼容性分析器
Microsoft 提供了 Roslyn 分析器来查找 .NET 7 或 .NET 8 项目中的 AOT 问题。它们可以在您编写代码时突出 显示 AOT 问题。但发布测试应用程序会检测到更多问题。
要使用静态分析,请将以下属性添加到 .csproj 文件:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
您需要以 .NET 7 或 .NET 8 框架为目标,才能在 .NET Standard 2.0 项目中使用此类分析。项目配置可能如下所示:
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<IsAotCompatible Condition="'$(TargetFramework)'=='net8.0'">true</IsAotCompatible>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
我们将上述配置应用于我们的大型核心项目,并收到 1036 个构建错误。
它们都与 AOT 兼容性有关吗?我们重建了没有 IsAotCompatible
属性的项目。但仍然有 1036 个错误。
所有错误都与 .NET 8 相对于 .NET Standard 2.0
的改进有关。在我们的案例中,大多数问题都与对可空引用类型的更好支持有关。还有许多关于新的
ArgumentOutOfRangeException.ThrowIfLessThan
和 ArgumentNullException.ThrowIfNull
辅助方法的 CA1500 和 CA1512 错误。
我们修复了所有错误并启用了 IsAotCompatible
属性。项目构建完成时没有出现 AOT
错误。静态分析没有捕获发布过程中发现的任何警告 🙁
为了确认,我们在代码库中添加了以下测试代码:
Type t = typeof(int);
t = typeof(List<>).MakeGenericType(t);
Console.WriteLine(Activator.CreateInstance(t));
string s = Console.ReadLine() ?? string.Empty;
Type? type = Type.GetType(s);
if (type != null)
{
foreach (var m in type.GetMethods())
Console.WriteLine(m.Name);
}
此 C# 代码与 .NET AOT 和修剪完全不兼容。静态分析发现了以下错误:
错误 IL3050:使用具有“RequiresDynamicCodeAttribute”的成员“System.Type.MakeGenericType(params Type[])”可能会在 AOT 编译时破坏功能。此实例化的 本机 代码可能在运行时不可用。
错误 IL2057:传递给方法“System.Type.GetType(String)”的参数“typeName”的值无法识别。无法保证目标类型的可用性。
因此,Roslyn 分析器可以工作并发现一些问题。在我们的案例中,他们没有发现任何问题。
捕获运行时错误
如何使库与 Native AOT 兼容 文章指出了以下原则:
如果应用程序在发布 AOT 时没有警告,那么它在 AOT 之后的行为将与没有 AOT 时相同。
在实践中,AOT 之后的版本仍然可以以不同的方式工作。我们已经修复了所有 AOT 和修剪警告。所有自动测试都通过了。测试应用程序正确处理了 PDF 文档。而同一应用程序的 AOT 版本生成了错误的文档。
AOT 发布错误地删除了一些必需的代码。我们创建了 优化删除了必要的代码 问题。.NET 团队迅速确认并修复了它。我们在 C# 代码中添加了一种解决方法,直到修复可用。
这个故事的寓意是,发布和静态分析警告可能还不够。在测试应用程序中使用项目的关键功能。并在部署后运行测试应用程序。
Docotic.Pdf 测试应用程序使用 PDF 压缩、文本提取和其他关键功能。我们这样使用它:
AotCompatibility.exe compress "C:\compressed.pdf" "C:\input.pdf" "optional-password"
手动测试已发布的应用程序并不方便。值得将这个过程自动化。
自动检测 AOT 问题
Docotic.Pdf 开发基于 自动测试。每个功能或错误都有测试。.NET AOT 兼容性也不例外。
有两种方法可以自动化测试 Native AOT 应用程序。
将测试置于 AOT 应用程序之外
在这里,您使用常规的 NUnit/xUnit/MSTest 项目。在构建此项目之前,您发布 AOT 测试应用程序。测试使用 Process.Start 方法运行已发布的应用程序。
主要优势是将测试基础架构与 AOT 应用程序分离。您可以使用单元测试框架的全部功能。测试框架中的任何 AOT 兼容性问题都无关紧要。重用现有测试用例也更容易。
测试项目的示例配置 可在 GitHub 上找到。Docotic.Tests
项目包含针对托管和
AOT 版本的自动测试。AotCompatibility
是用于 AOT 发布的测试应用程序。
AotCompatibility.csproj
在 Release
配置中的构建后事件上发布测试应用程序。当您使用 Visual Studio
中的“发布”向导时,'$(PublishProtocol)'==''
条件会禁用此步骤。
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release' And '$(PublishProtocol)'==''">
<Exec Command=""$(SolutionDir)Scripts\publish.bat"" />
</Target>
publish.bat
脚本很简单:
@echo off
SET AOT_TEST_DIR=%~dp0\..\AotCompatibility
dotnet publish "%TRIM_TEST_DIR%\AotCompatibility.csproj" --no-build /warnaserror /p:PublishProfile="%AOT_TEST_DIR%\Properties\PublishProfiles\win-x64-installer.pubxml"
请注意,我们在这里使用了 --no-build
参数。该项目在构建后事件之前已经构建。我们不需要再次构建它。
NativeAot.cs
中的测试使用不同的参数运行已发布的 AotCompatibility.exe
。Docotic.Tests
和
AotCompatibility
项目共享相同的测试代码。这使我们能够检查托管和 AOT 版本而无需重复代码。
将测试放在 AOT 应用程序内
另一种方法是在 AOT 测试应用程序内编写测试。2024 年 7 月,您只能使用 MS Test 早期预览版。阅读 测试您的 Native AOT 应用程序 文章了解更多详情。
关键问题是,测试框架中的任何 AOT 问题都会使您的测试不可靠。而且只有一组有限的测试框架功能可用。这就是为什么我们更喜欢将测试基础架构放在 AOT 应用程序之外。
修复 AOT 和修剪警告
避免抑制发布警告。通常,抑制是一种不正确的解决方案。当您抑制 AOT 或修剪警告时,您说您的项目与 AOT 兼容。但是,警告的根本原因仍然存在。任何被抑制的警告最终都可能导致应用程序运行时失败。
尝试删除或重写与每个警告相关的代码。更新第三方组件也可能有帮助。
让我们回顾一下我们如何修复 Docotic.Pdf 中的 AOT 和修剪警告。您已经在 获取发布警告 部分中看到了其中一些。
从 LibLog 中修剪警告
我们使用 LibLog 库进行日志记录。LibLog 依靠反射来检测和使用流行的日志记录框架。它在设计上与 AOT 不兼容。
目前,Microsoft.Extensions.Logging
包是 .NET 中日志记录的标准。LibLog 开发现已冻结。
鉴于此,我们已将 LibLog 从代码中完全移除。相反,我们发布了
Docotic.Pdf.Logging 插件。它依赖于
Microsoft.Extensions.Logging.Abstractions
接口。这减少了核心库的大小并修复了所有相关的修剪警告。
Docotic.Layout 附加组件中的调试代码
在 Docotic.Layout 附加组件 中,我们使用反射进行内部调试。这导致了警告 IL2075。
代码库中存在相应的代码,但客户端无法通过公共 API 使用它。解决方案是从 Release
配置中排除代码。它现在仅在 Debug
配置中使用。
BouncyCastle 的 AOT 警告 IL3050
BouncyCastle 库可帮助我们签署 PDF 文档。以下警告来自 BouncyCastle 代码:
Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): 使用具有“RequiresDynamicCodeAttribute”的成员“System.Enum.GetValues(Type)”可能会在 AOT 编译时破坏功能。可能无法在运行时创建枚举类型的数组。请改用 GetValues<TEnum> 重载或 GetValuesAsUnderlyingType 方法。
BouncyCastle 使用 Enum.GetValues(Type) 方法,该方法不兼容 AOT。
最简单的解决方案是改用 Enum.GetValues<T>()
重载。最新的 BouncyCastle 版本已经使用它了。不幸的是,此重载仅在
.NET 5 或更新版本中可用。这不是 .NET Standard 2.0 的选项。
我们深入挖掘并分析了 BouncyCastle 代码。结果发现 BouncyCastle 使用它来防止枚举常量的混淆。相反,我们使用
[System.Reflection.Obfuscation(Exclude = true)]
属性禁用了相应枚举的混淆。并删除了不再需要使用的
Enum.GetValues(Type)
。
混淆构建中的一个奇怪问题
我们使用混淆来保护生产版本。令人惊讶的是,混淆版本的 .NET AOT 部署返回了类似的警告:
<unknown>:0: 错误:符号“__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon”已定义
此类错误没有关联的 IL30##
或 IL2###
代码。不清楚如何识别关联的代码。
错误只发生在混淆版本中。这可能是混淆器中的一个错误。我们已经更新了混淆器,但错误仍然存在。
我们需要缩小问题的范围。我们使用了以下基于二分查找的过程:
- 禁用一半命名空间的混淆。检查是否发生错误。继续,直到找到导致错误的命名空间。
- 禁用第一步中命名空间中一半类型的混淆。检查是否发生错误。继续,直到找到导致错误的类型。
- 禁用第二步中类型中一半成员的混淆。检查是否发生错误。继续,直到找到导致错误的成员。
- 查看第三步中成员的代码。注释掉不寻常的部分并检查是否发生错误。
我们最终进入了第一步 🙂 在禁用 所有 命名空间的混淆后,仍然会出现错误。
我们用 ILSpy 分析了生成的程序集。发现了一些意外的编译器生成类型。ILSpy 显示了这些类型的用法,这些类型来自这样的 C# 代码:
interface X
{
void f();
}
X obj = ..;
if (obj.f != null)
..
这个奇怪的代码来自 将旧式 C 代码迁移到 C#。if (obj.f != null)
条件完全是多余的。我们删除了这些条件,错误就消失了。
不要使用 GetCallingAssembly
我们发现了另一个 运行时问题。AOT 发布后,LicenseManager.AddLicenseData(string) 方法调用失败,并出现以下错误:
Assembly.GetCallingAssembly() 抛出 System.PlatformNotSupportedException:此平台不支持操作。
.NET 9 中的 Assembly.GetCallingAssembly
方法 应该为 Native AOT
实现。在修复完成之前,我们重构了代码以尽量减少负面影响。现在,我们仅使用调用程序集属性来验证
Application 许可证。其他许可证类型与 AOT 兼容。
使用特殊属性标记代码
幸运的是,我们已经能够修复核心 Docotic.Pdf 库和大多数附加组件中的所有 AOT 和修剪警告。但是,您可能无法重写所有 AOT 不兼容的代码。
.NET 为这种情况提供了特殊属性。您可以使用属性标记 API,以告知客户端已知的 AOT 问题。您的 API 的用户在调用标记的方法时会收到警告。
标记 AOT 不兼容 .NET 代码的关键属性是:
RequiresDynamicCode
RequiresUnreferencedCode
DynamicallyAccessedMembers
您可以在官方的 AOT 警告简介 和 为修剪准备 .NET 库 文章中找到有关这些属性的更多信息。
结论
Native AOT 部署是 .NET 世界中的一大进步。您可以编写常规 C# 代码并获得本机应用程序或库。此类应用程序通常速度更快,无需安装 .NET 运行时即可运行。您甚至可以使用 C、Rust 或其他非 .NET 编程语言中发布的 DLL。
发布测试 .NET 8 应用程序以查找 AOT 兼容性问题。还有 Roslyn 分析器,但它们捕获的问题较少。
核心 Docotic.Pdf 库现在兼容 AOT 和修剪。以下附加组件也兼容:
- Layout 附加组件
- Gdi 附加组件
- Logging 附加组件
HTML 转 PDF 插件仍包含修剪警告。这是我们在 Native AOT 路线图上的下一个目标。
欢迎随时向我们询问有关 Native AOT 或 PDF 处理的问题。