Go to my github

【译】如何使库与本机 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/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com)

posted @ 2024-02-17 11:01  郑子铭  阅读(272)  评论(0编辑  收藏  举报