该页面可以包含自动翻译的文本。
如何在 .NET 中开发 Native AOT 应用程序
.NET 8 为提前编译提供了全面支持,也称为 Native AOT。此类应用程序通常启动更快,并且比托管方案占用更少内存。
.NET AOT 发布会生成裁剪后的自包含应用程序。此类应用程序:
- 可以在未安装 .NET runtime 的机器上运行。
- 面向特定运行时环境,例如 Windows x64。
- 不包含未使用的代码。

我们开发 Docotic.Pdf 库用于 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。编译到 AOT 时会产生裁剪警告:
warning IL2104: Assembly 'BitMiracle.Docotic.Pdf' produced trim warnings. For more information see 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 允许你获取关于警告的详细信息。否则,只会得到“Assembly 'X' produced trim warnings”消息。
下面是 Program.cs 中的 C# 代码:
using BitMiracle.Docotic.Pdf;
using var pdf = new PdfDocument();
添加一些代码来强制加载被检查的程序集。要获取发布警告,并不需要覆盖项目的全部功能。使用单个类型就足够了。但要编写更多代码来捕获运行时错误。
该项目运行 dotnet publish -r win-x64 -c Release 命令后返回了裁剪和 AOT 警告。以下是列表的简短版本:
AOT analysis warning IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling.
Trim analysis warning IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): Using member 'System.Linq.Expressions.Expression.Property(Expression,String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.
Trim analysis warning IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'.
Trim analysis warning IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The parameter 'loggerType' of method 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
Trim analysis warning IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'LogProviders.LogProviderBase.FindType(String,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
我们找到了根本原因。在修复这些问题之前,先看看查找 AOT 兼容性问题的其他可选方案。
使用 AOT 兼容性分析器
Microsoft 提供了 Roslyn 分析器,用于查找 .NET 7 或 .NET 8 项目中的 AOT 问题。它们可以在编写代码时突出显示 AOT 问题。不过,发布测试应用程序能检测到更多问题。
要使用静态分析,请在 .csproj 文件中添加以下属性:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
如果要在 .NET Standard 2.0 项目中使用此类分析,需要将目标框架设为 .NET 7 或 .NET 8。项目配置可能如下:
<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 和裁剪 100% 不兼容。静态分析识别出了以下错误:
error IL3050: Using member 'System.Type.MakeGenericType(params Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.
error IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'. It's not possible to guarantee the availability of the target type.
因此,Roslyn 分析器确实有效,也能发现一些问题。但在我们的案例中,它们没有发现任何问题。
捕获运行时错误
如何使库兼容 native AOT 这篇文章给出了以下原则:
If an application has no warnings when being published for AOT, it will behave the same after AOT as it does without AOT.
但在实践中,AOT 之后的版本仍可能表现不同。我们已经修复了所有 AOT 和裁剪警告。所有自动化测试都通过了。测试应用程序能够正确处理 PDF 文档。而同一应用程序的 AOT 版本却生成了错误的文档。

AOT 发布错误地删除了一些所需代码。我们创建了 Optimization removes necessary code 问题。 .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 配置下通过构建后事件发布测试应用程序。'$(PublishProtocol)'=='' 条件会在你使用 Visual Studio 的“Publish”向导时禁用此步骤。
<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 扩展包中的调试代码
在布局扩展包中,我们使用反射进行内部调试。这导致了裁剪分析警告 IL2075。
相应代码仍存在于代码库中,但客户端无法通过公共 API 使用它。解决方案是在 Release 配置下排除这些代码。现在它只在 Debug 配置中使用。
来自 BouncyCastle 的 AOT 警告 IL3050
BouncyCastle 库帮助我们对 PDF 文档进行签名。以下警告来自它的代码:
Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues<TEnum> overload or the GetValuesAsUnderlyingType method instead.
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: error: symbol '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' is already defined
这类错误没有关联的 IL30## 或 IL2### 编号,因此不清楚如何定位对应代码。
错误只出现在混淆版本中。这可能是混淆器的 bug。我们升级了混淆器,但错误仍然存在。
我们需要缩小问题范围。我们使用了基于二分查找的以下流程:
- 关闭一半命名空间的混淆。检查是否出现错误。继续,直到找到导致错误的命名空间。
- 关闭步骤一中命名空间内一半类型的混淆。检查是否出现错误。继续,直到找到导致错误的类型。
- 关闭步骤二中类型内一半成员的混淆。检查是否出现错误。继续,直到找到导致错误的成员。
- 审查步骤三中该成员的代码。注释掉异常部分并检查是否仍然出现错误。
结果我们停在了步骤一 🙂 在关闭所有命名空间的混淆后,错误仍然存在。
我们使用 ILSpy 分析了生成的程序集。发现了一些意料之外的编译器生成类型。ILSpy 展示了它们在类似 C# 代码中的用法:
interface X
{
void f();
}
X obj = ..;
if (obj.f != null)
..
这段奇怪的代码来自将旧式 C 代码迁移到 C#。if (obj.f != null) 条件完全是多余的。我们删除了这些条件,错误就消失了。
不要使用 GetCallingAssembly
我们捕获到了另一个运行时问题。调用 LicenseManager.AddLicenseData(string) 方法在 AOT 发布后失败,并出现以下错误:
Assembly.GetCallingAssembly() throws System.PlatformNotSupportedException: Operation is not supported on this platform.
Assembly.GetCallingAssembly 方法应在 .NET 9 中为 Native AOT 实现。在修复完成之前,我们重构了代码以尽量减小负面影响。现在,我们只使用调用程序集特性来验证应用程序许可证。其他许可证类型与 AOT 兼容。
使用特殊属性标记代码
幸运的是,我们已经能够修复核心 Docotic.Pdf 库以及大多数扩展包中的所有 AOT 和裁剪警告。但你也许无法重写所有与 AOT 不兼容的代码。
.NET 为此类情况提供了特殊属性。你可以用这些属性标记 API,以便向客户端说明已知的 AOT 问题。调用被标记方法的 API 用户会收到警告。
用于标记与 AOT 不兼容的 .NET 代码的关键属性有:
RequiresDynamicCodeRequiresUnreferencedCodeDynamicallyAccessedMembers
有关这些属性的更多信息,请参阅官方的Introduction to AOT warnings 和Prepare .NET libraries for trimming 文章。
结论
Native AOT 部署是 .NET 世界的重要进步。你可以编写常规 C# 代码,并获得原生应用程序或库。这类应用程序通常更快,并且可以在未安装 .NET runtime 的情况下运行。你甚至可以从 C、Rust 或其他非 .NET 编程语言中使用已发布的 DLL。
发布一个 .NET 8 测试应用程序来查找 AOT 兼容性问题。也有 Roslyn 分析器,但它们发现的问题更少。
核心 Docotic.Pdf 库现在已兼容 AOT 和裁剪。以下扩展包也兼容:
- Conformance 扩展包
- Layout 扩展包
- Gdi 扩展包
- Logging 扩展包
HTML to PDF 扩展包仍然包含裁剪警告。这是我们在 Native AOT 路线图上的下一个目标。
欢迎向我们提问,了解 Native AOT 或 PDF 处理相关问题。