當前位置: 妍妍網 > 碼農

【譯】如何使庫與本機 AOT 相容(二)

2024-02-18碼農

點選上方「DotNet NB」 關註公眾號

1

/

D o t N e t N B 230

原文 | Eric Erhardt

轉譯 | 鄭子銘

開放式遙測

OpenTelemetry 是一個可觀察性框架,允許開發人員從外部了解他們的系統。它在雲應用程式中很流行,並且是雲原生計算基金會的一部份。.NET OpenTelemetry 庫必須修復一些地方才能與 AOT 相容。open-telemetry/opentelemetry-dotnet#3429 是跟蹤必要修復的主要 GitHub 問題。

第一個阻止該庫在本機 AOT 應用程式中使用的修復是 open-telemetry/opentelemetry-dotnet#4542。問題是使用工具無法靜態分析的值型別呼叫 MakeGenericType。

當呼叫 RegisterSlot () 或 RegisterSlot () 時,此程式碼使用反射動態填充泛型型別,然後呼叫 ContextSlotType 的建構函式。由於此 API 是公共的,因此可以在 ContextSlotType 上設定任何開放的通用型別。然後任何值型別都可以填充到 RegisterSlot 方法中。

修復方法是進行一個小的重大更改,並且只接受在 ContextSlotType 上設定 2 或 3 個特定型別,這實際上是客戶使用的唯一型別。

這些型別是寫死的,因此不會被刪除。現在,AOT 工具可以看到完成這項工作所需的所有程式碼。

另一個問題是如何在 ActivityInstrumentationHelper 類中使用 System.Linq.Expressions。這是使用私有反射來解決沒有公共 API 的另一種情況。open-telemetry/opentelemetry-dotnet#4513 更改了運算式程式碼以確保保留必要的內容。

修剪工具無法靜態確定 Expression.Property(Expression, string propertyName) 參照了哪個內容,並且 API 已被註釋以在呼叫它時生成警告。相反,如果您使用多載 Expression.Property(Expression, PropertyInfo) 並以工具可以理解的方式獲取 PropertyInfo,則可以使程式碼修剪相容。

然後使用 open-telemetry/opentelemetry-dotnet#4695 完全刪除庫中的 System.Linq.Expressions 使用。

雖然運算式可以在本機 AOT 應用程式中使用,但當您 Lambda.Compile() 運算式時,它會使用直譯器來計算運算式。這並不理想,並且可能導致效能下降。如果可能,建議在本機 AOT 應用程式中刪除 Expression.Compile() 的使用。

接下來是修剪警告的常見誤報案例。使用 EventSource 時,通常會將 3 個以上的原始值或不同型別的值傳遞給 WriteEvent 方法。但是,當您與原始多載不匹配時,您就會陷入使用 object[] args 作為參數的多載。由於這些值是使用反射進行序列化的,因此該 API 帶有 [RequiresUnreferencedCode] 註釋,並在呼叫時發出警告。開啟 open-telemetry/opentelemetry-dotnet#4428 以添加這些抑制。

這種誤報發生的頻率非常高,因此 .NET 8 中的 EventSource 中的新 API 使這種誤報幾乎完全消失。

open-telemetry/opentelemetry-dotnet#4688 中進行了另一個簡單的修復,以使 [DynamicallyAccessedMembers] 內容透過庫。例如:

接下來,OpenTelemetry 中的幾個匯出器使用 JSON 序列化將物件陣列轉換為字串。如前所述,在沒有 JsonTypeInfo 的情況下使用 JsonSerializer.Serialize 與修剪或 AOT 不相容。open-telemetry/opentelemetry-dotnet#4679 將這些位置轉換為使用 OpenTelemetry 中的 System.Text.Json 源生成器。

internalstaticstringJsonSerializeArrayTag(Array array)
{
return JsonSerializer.Serialize(array, typeof(Array), ArrayTagJsonContext.Default);
}
[JsonSerializable(typeof(Array))]
[JsonSerializable(typeof(char))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(byte))]
[JsonSerializable(typeof(sbyte))]
[JsonSerializable(typeof(short))]
[JsonSerializable(typeof(ushort))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(ulong))]
[JsonSerializable(typeof(float))]
[JsonSerializable(typeof(double))]
privatesealedpartial classArrayTagJsonContext : JsonSerializerContext
{
}

現在可以在AOT應用程式中安全地使用此Jsonserializearraytag方法。請註意,它不支持任何物件序列化 - 僅支持陣列和列出的原始型別。如果將不支持的物件傳遞到此方法中,則在應用程式的情況下,它將始終如一地失敗。

更復雜的更改之一是open-telemetry/opentelemetry-dotnet#4675,它使內容fetcher類與本機AOT相容。顧名思義,內容fetcher的專門設計用於從物件中檢索內容值。它大量使用反射和制作型。因此,最終仍然用[requiensunreferencedCode]註釋。呼叫者的責任是確保手動保留必要的內容。幸運的是,此API是內部的,因此OpenTelemetry團隊控制所有呼叫者。

PropertyFetcher的其余問題是確保MakeErictype呼叫始終在本機AOT應用程式中起作用。

這裏的緩解措施利用了以下事實:如果僅使用參考型別(即型別而不是結構)呼叫MakeGenerictype,則.NET執行時將重用所有參考型別的相同機器碼。

現在,該內容開采已更改為與本機AOT一起工作,現在可以解決的地方可以解決。OpenTelemetry所需的方法之一是收聽診斷程式,註冊事件何時啟動的回呼,然後檢查事件的「有效負載」,以記錄相應的遙測事件。有3個執行此操作並使用PropertyFetcher的儀器庫。

  • httpclient - open-telemetry/opentelemetry-dotnet#4770

  • ASP.NET核心 - open-telemetry/opentelemetry-dotnet#4795

  • SQL客戶端 - open-telemetry/opentelemetry-dotnet#4751

  • 前2個PR能夠抑制裝飾警告,因為基礎診斷程式碼(HttpClient 和 ASP.NET Core)可確保有效載荷上的重要內容保留在修剪和AOT應用程式中。

    對於SQL客戶端,情況並非如此。而且,由於基礎SQLCLCLIENT庫不相容,因此決定將OpenTElemetry.SqlClient庫標記為[quiendunreferencedCode]。

    最後,open-telemetry/opentelemetry-dotnet#4859 修復了OpentElemetry.exporter.opentelemetryprotocol庫中的最後一個警告。

    這裏的問題與上面 StackExchange.Redis 庫中的問題相同。此程式碼對 Google.Protobuf 庫中的物件使用私有反射,並生成 DynamicMethod 以提高效能。較新版本的 Google.Protobuf 添加了 .Clear() API,這使得不再需要此私有反射。因此,修復方法很簡單,就是更新到新版本,並使用新的 API。

    dotnet/擴充套件

    https://github.com/dotnet/extensions 中的新 Microsoft.Extensions.* 庫填補了構建真實世界、大規模和高可用性應用程式所需的一些缺失場景。有一些庫可以增加應用程式的彈性、更深入的診斷和合規性。

    這些庫利用其他 Microsoft.Extensions.* 功能,即將 Option 物件繫結到 IConfiguration 並使用 System.ComponentModel.DataAnnotations 內容驗證 Option 物件。傳統上,這兩個功能都使用無界反射來獲取和設定 Option 物件的內容,這與修剪不相容。為了允許在精簡的應用程式中使用這些功能,.NET 8 添加了兩個新的 Roslyn 源生成器。

  • 選項驗證

  • 配置繫結

  • dotnet/extensions 庫的初始送出已經使用了選項驗證源生成器。要使用此源生成器,您需要建立一個實作 IValidateOptions 的分部類並套用 [OptionsValidator] 內容。

    [OptionsValidator]
    internalsealedpartial classHttpStandardResilienceOptionsValidator : IValidateOptions<HttpStandardResilienceOptions>
    {
    }

    源生成器將在構建時檢查 HttpStandardResilienceOptions 型別的所有內容,尋找 System.ComponentModel.DataAnnotations 內容。對於它找到的每個內容,它都會生成程式碼來驗證內容的值是否可接受。

    然後可以使用依賴項註入 (DI) 註冊驗證器,以將其添加到應用程式中的服務中。

    在這種情況下,驗證器被註冊為在應用程式啟動時立即執行,而不是在第一次使用 HttpStandardResilienceOptions 時執行。這有助於在網站接受流量之前發現配置問題。它還確保第一個請求不需要產生此驗證的成本。

    dotnet/extensions#4625 為 dotnet/extensions 庫啟用了配置繫結程式源生成器,並修復了另一個小 AOT 問題。

    要啟用配置聯編程式源生成器,可以在計畫中設定一個簡單的 MSBuild 內容:

    <PropertyGroup>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
    </PropertyGroup>

    啟用後,此源生成器會尋找對 Microsoft.Extensions.Configuration.ConfigurationBinder 的所有呼叫,並生成用於根據 IConfiguration 值設定內容的程式碼,因此不再需要反射。呼叫將重新路由到生成的程式碼,並且不需要修改現有程式碼。這允許繫結在修剪的應用程式中工作,因為每個內容都是由程式碼顯式設定的,因此它們不會被修剪。

    最後,一些程式碼檢查列舉的所有值。在 .NET 的早期版本中,執行此操作的方法是呼叫 Enum.GetValues(typeof(MyEnum))。但是,該 API 與 AOT 不相容,因為需要在執行時建立 MyEnum 陣列,並且 AOT 程式碼可能不包含 MyEnum[] 的特定程式碼。

    修復方法是在支持它的目標框架上執行時利用相對較新的 API:Enum.GetValues ()。此 API 確保生成 TEnum[] 程式碼。當不在新的 .NET 目標框架上時,程式碼將繼續使用舊的 API。

    Dapper

    Dapper 是一個簡單的微型 ORM,用於簡化 ADO.NET 的使用。它的工作原理是在執行時基於所使用的 ADO.NET 庫(例如 Microsoft.Data.SqlClient 或 Npgsql)以及應用程式中使用的強型別(客戶、訂單等)生成動態 IL。這可以減少鍋爐的工作量-應用程式中將物件讀/寫到資料庫所需的板程式碼。

    有時,您的庫中只有少數 API 與本機 AOT 不相容。您可以將它們歸為此類,並添加專為 AOT 相容性而設計的新 API。但就 Dapper 而言,其核心設計本質上與原生 AOT 不相容。在執行時生成 IL 與使用原生 AOT 的原因完全相反。因此,Dapper 無法修改以支持本機 AOT。

    但它支持的場景仍然很重要,並且使用 Dapper 的開發人員體驗比使用純 ADO.NET API 好得多。為了實作這種體驗,需要新的設計。

    輸入 Dapper.AOT,它是 Dapper 的重寫版本,它在構建時生成 ADO.NET 程式碼,而不是在執行時動態生成 IL。在與本機 AOT 相容的同時,這還減少了非 AOT 應用程式的啟動時間,因為程式碼已經生成並編譯,無需在應用程式啟動時生成它。

    深入探討這是如何實作的,值得單獨寫一篇部落格文章,並且您可以在文件中找到簡短的解釋。如果您發現自己需要完全重寫庫才能使用 Roslyn 源生成器,請檢視源生成器入門文件。盡管開發成本高昂,但源生成器可以消除使用無界反射或在執行時生成 IL 的必要性。

    從不支持原生 AOT

    有些 .NET 程式碼永遠不會支持本機 AOT。庫可能存在本質上的基本設計,使其不可能相容。一個例子是可延伸性框架,例如托管可延伸性框架。該庫的全部目的是在執行時載入原始可執行檔不知道的擴充套件。這就是 Visual Studio 的可延伸性的構建方式。您可以為 Visual Studio 構建外掛程式來擴充套件其功能。此場景不適用於本機 AOT,因為擴充套件可能需要從原始應用程式中刪除的方法(例如 string.Replace)。

    Newtonsoft.Json 屬於庫可能決定不支持本機 AOT 的另一種情況。圖書館需要考慮現有客戶。如果不進行重大更改,使現有 API 相容可能是不可行的。這也將是一項相當大的工作量。在這種情況下,有一個已經相容的替代方案。所以這裏的好處可能不值得付出代價。

    開誠布公地告訴客戶您的目標和計劃對客戶很有幫助。這樣客戶就可以了解他們的應用程式和庫並為其制定計劃。如果您不打算在圖書館中支持本機 AOT,請告訴客戶,讓他們知道制定替代計劃。如果這需要大量工作,但最終可能會發生,那麽了解這些資訊也很有幫助。在我看來,有效的溝通是軟體開發中最有價值的特質之一。

    概括

    Native AOT正在擴充套件.NET可以成功使用的場景。與傳統的獨立 .NET 應用程式相比,應用程式可以更快地啟動,使用更少的記憶體,並且磁盤大小更小。但為了讓應用程式使用這種新的部署模型,它們使用的庫需要與本機 AOT 相容。

    我希望您發現本指南有助於使您的庫與本機 AOT 相容。

    原文連結

    How to make libraries compatible with native AOT

    推薦閱讀:

    點選下方卡片關註DotNet NB

    一起交流學習

    點選上方卡片關註DotNet NB,一起交流學習

    請在公眾號後台

    回復 【路線圖】 獲取.NET 2023開發者路線圖

    回復 【原創內容】 獲取公眾號原創內容

    回復 【峰會視訊】 獲取.NET Conf開發者大會視訊

    回復 【個人簡介】 獲取作者個人簡介

    回復 【年終總結】 獲取作者年終總結

    回復 加群 加入DotNet NB 交流學習群

    長按辨識下方二維碼,或點選閱讀原文。 和我一起,交流學習,分享心得。