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

原文 | 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 源生成器。

internal static string JsonSerializeArrayTag(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))]
private sealed partial class ArrayTagJsonContext : JsonSerializerContext
{
}

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

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

PropertyFetcher的其餘問題是確保MakeErictype調用始終在本機AOT應用程序中起作用。

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

現在,該屬性開採已更改爲與本機AOT一起工作,現在可以解決的地方可以解決。 OpenTelemetry所需的方法之一是收聽診斷程序,註冊事件何時啓動的回調,然後檢查事件的“有效負載”,以記錄相應的遙測事件。有3個執行此操作並使用PropertyFetcher的儀器庫。

前2個PR能夠抑制裝飾警告,因爲基礎診斷代碼(HttpClientASP.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]
internal sealed partial class HttpStandardResilienceOptionsValidator : 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

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。

如有任何疑問,請與我聯繫 ([email protected])

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章