該頁面可以包含自動翻譯的文字。

如何在.NET中開發Native AOT應用程式

.NET 8 帶來了對提前編譯的全面支持,也稱為Native AOT。與託管解決方案相比,此類應用程式通常速度更快,消耗的記憶體更少。

.NET AOT 發布產生經過修剪的、獨立的應用程式。此類應用:

  1. 可以在未安裝.NET執行時的機器上執行。
  2. 針對特定的執行環境,例如Windows x64。
  3. 請勿包含未使用的代碼。

部署 .NET AOT 應用程式

我們開發用於 PDF 處理的 Docotic.Pdf 庫。 AOT 相容性是 2024 年最受歡迎的功能請求之一。您將學習如何尋找和修復 .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 8 中,您也可以發布 ASP.NET Core 應用程式。

但 Docotic.Pdf 核心庫面向 .NET Standard 2.0。我們能讓它對 AOT 友善嗎?當然。

您仍然可以在面向 .NET Standard、.NET 5 或 .NET 6 的 .NET 程式庫中尋找並修復 AOT 相容性問題。Native AOT 應用程式可以依賴該程式庫。

尋找 AOT 問題

我們關於 AOT 相容性的第一個支援請求聽起來像這樣:

我們正在考慮使用 Docotic.Pdf 編譯為 WebAssembly。當我們將其編譯為 AOT 時,它會產生修剪警告:
警告 IL2104:程式集「BitMiracle.Docotic.Pdf」產生修剪警告。有關詳細信息,請參閱 https://aka.ms/dotnet-illink/libraries

資訊不是很豐富。讓我們重現問題並獲取有關這些修剪警告的更多詳細資訊。

取得發布警告

發布測試應用程式是查找 .NET AOT 問題的最靈活的方法。你需要:

  1. 使用 PublishAot = true 建立一個 .NET 8 控制台項目
  2. 添加對您的項目的引用並使用它的一些類型

這是我們用於 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):「this」參數不符合呼叫「System.Type.GetProperty(String)」時的「DynamicallyAccessedMemberTypes.PublicProperties」。方法「LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)」的參數「loggerType」沒有相符的註解。來源值必須至少聲明與其分配到的目標位置上聲明的要求相同的要求。

修剪分析警告 IL2075:LogProviders.Log4NetLogProvider.GetOpenNdcMethod():「this」參數不符合呼叫「System.Type.GetProperty(String)」時的「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.ThrowIfLessThanArgumentNullException.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% 不相容。靜態分析發現以下錯誤:

錯誤 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應用程式仍然無法運行

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.csprojRelease配置中的建置後事件上發布測試應用程式。當您在 Visual Studio 中使用「發佈」精靈時,'$(PublishProtocol)'=='' 條件會停用此步驟。

<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release' And '$(PublishProtocol)'==''">
    <Exec Command="&quot;$(SolutionDir)Scripts\publish.bat&quot;" />
</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.exeDocotic.TestsAotCompatibility 專案共用相同的經過測試的程式碼。這使我們能夠檢查託管版本和 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 add-on中,我們使用反射進行內部調試。這導致了警告 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 部署回傳了類似的警告:

<未知>:0:錯誤:符號「__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon」已定義

此類錯誤沒有關聯的IL30##IL2###代碼。目前尚不清楚如何識別相關代碼。

錯誤僅發生在混淆版本中。這可能是混淆器中的一個錯誤。我們已經更新了混淆器,但錯誤仍然存在。

我們需要縮小問題的範圍。我們使用了以下基於二分搜尋的過程:

  1. 禁用一半命名空間的混淆。檢查是否出現錯誤。繼續下去,直到找到導致錯誤的名稱空間。
  2. 停用第一步中命名空間中一半類型的混淆。檢查是否出現錯誤。繼續下去,直到找到導致錯誤的類型。
  3. 停用第二步驟中類型中一半成員的混淆。檢查是否出現錯誤。繼續下去,直到找到導致錯誤的成員。
  4. 查看第三步驟中的會員代碼。註解掉不正常的部分,檢查是否有錯誤。

我們結束了第一步 🙂 在禁用_所有_名稱空間的混淆後,錯誤仍然發生。

我們使用 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 處理的問題。