mcpField
A
aot-trimming

aot-trimming

github.com/aaronontheweb开发1.2k54 次安装今天更新
代码仓库

关于 aot-trimming

aot-trimming 是面向 .NET 开发者的参考技能,帮助库和应用兼容裁剪与 Native AOT。它说明链接器注解、运行时反射的替代方案,以及通过发布构建检查警告的流程。

主要功能

  1. 01解释裁剪与 Native AOT 的行为
  2. 02指导使用链接器注解
  3. 03介绍反射安全的实现模式
  4. 04说明源生成器和动态代码边界
  5. 05提供发布与警告验证清单

适用场景

  1. 01使 .NET 库兼容裁剪
  2. 02解决 IL2xxx 或 IL3xxx 警告
  3. 03审查代码库的 AOT 兼容性
  4. 04用编译期方案替代运行时反射

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/aaronontheweb/dotnet-skills --skill aot-trimming

安装后在 Claude Code 中输入/aot-trimming确认已加载。

附带文件aot-trimming-playbook-reference.mdtrimming-attributes-reference.md

详细介绍

.NET 裁剪与 Native AOT

适用场景

  • 为库或应用添加裁剪(PublishTrimmed)或 Native AOT(PublishAot)支持
  • 解决 IL2xxx(裁剪)和 IL3xxx(AOT/单文件)警告
  • 让大量使用反射的代码能够安全地进行裁剪
  • 用编译时替代方案取代运行时反射、程序集扫描或 Reflection.Emit
  • 审查代码库或 PR 的裁剪/AOT 兼容性,包括生成的拦截器和分析器抑制器
  • 设计必须携带裁剪注解的公共 API

核心目标

  • 生成能够通过链接器可达性分析并在 Native AOT(无 JIT)下运行的代码。
  • 使用裁剪属性说明编译器无法获知的信息——反射需要哪些成员。
  • 将裁剪安全代码与不安全代码分开,使不安全路径明确且范围受限。

核心模型

裁剪与 Native AOT
  • 裁剪(PublishTrimmed)会运行 ILLink 可达性分析:仅保留静态可达的代码。分析器无法识别的反射调用所需代码会被裁剪掉,并产生警告。
  • Native AOT(PublishAot)要求必须进行裁剪,并移除 JIT。Reflection.Emit 不受支持,运行时构造未知泛型实例无法保证可行,Expression.Compile() 可能会回退到解释执行,而反射只能访问链接器保留的成员。
  • 两者运行相同的静态分析:
    • IL2xxx — 裁剪警告:RequiresUnreferencedCode 传播 + DynamicallyAccessedMembers 数据流分析。
    • IL3xxx — AOT 和单文件警告:RequiresDynamicCode / RequiresAssemblyFiles。

库可以支持裁剪,但不兼容 AOT(它使用 Reflection.Emit,裁剪可以容忍,但 AOT 不支持)。

需要解决的两个问题
  1. 无法识别的反射调用(裁剪):链接器会移除通过名称加载或在泛型方法中通过 typeof(T) 访问的成员或类型。
  2. 动态代码生成(AOT):JIT 已移除,因此不支持 Reflection.Emit,并且无法保证能够构造未知泛型实例。

解决这两个问题的方法相同:让分析器能够静态识别所需成员,或者彻底消除对反射的需求。

反射并非问题所在

反射是一种合理的技术,在普通的 JIT 应用中完全没有问题——这类应用不需要这些措施。裁剪属性旨在允许在裁剪/AOT 环境中使用反射,而非禁止反射:为反射所需内容添加注解,链接器就会保留它们。只有当你希望移除或限制特定的反射接口时(例如,不想让公共注册 API 带有 [RequiresUnreferencedCode] 一起发布),才应考虑源生成器、[UnsafeAccessor] 或能力门控。

项目配置

对于库,声明兼容性,以便分析器报告警告,并让使用者了解:

<PropertyGroup>
  
  <IsTrimmable>true</IsTrimmable>

  
  <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>

对于应用,发布裁剪版本或 AOT 版本:

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  
  <PublishAot>true</PublishAot>
</PropertyGroup>

务必通过实际发布进行验证——裁剪/AOT 应用必须不产生任何警告:

dotnet publish -c Release -r <rid> -p:PublishTrimmed=true
dotnet publish -c Release -r <rid> -p:PublishAot=true

以库为根目录的测试应用是验证库的标准做法:

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
  <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
  <TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>

完整的属性表、功能开关和警告批准基线模式,请参阅 aot-trimming-playbook-reference.md。

特性模型

这些特性位于 System.Diagnostics.CodeAnalysis 中。完整目录及精确签名、警告代码和规则,请参阅 trimming-attributes-reference.md。

  • [RequiresUnreferencedCode(message)] — 标记需要链接器无法看到的成员的代码。在代码内部抑制警告;在每个调用点发出 IL2026。
  • [RequiresDynamicCode(message)] — 标记需要 JIT(发出代码、动态生成)的代码。在调用点发出 IL3050。
  • [DynamicallyAccessedMembers(memberTypes)] — 声明必须保留 Type/string 的哪些成员。它会从反射位置向后流向 Type 源。可将此特性(以及其他特性)直接放在访问器上,或使用 [method:]、[field:] 或 [return:] 目标,将其作用域限定为单个访问器。
  • [UnconditionalSuppressMessage(category, checkId, Justification = "...")] — 最后的补救手段,抑制信息会持久化到 IL 中;必须附带理由和真实的不变量。
  • [DynamicDependency("Member", typeof(T))] — 保留指定名称的成员,但不会抑制警告。

针对实际执行的反射,选择最窄的 DynamicallyAccessedMemberTypes:

反射成员类型
Activator.CreateInstance<T>()、new T()、Activator.CreateInstance(type)PublicParameterlessConstructor
DI 激活(ActivatorUtilities.CreateFactory<T>)PublicConstructors
显式激活非公共成员NonPublicConstructors
type.GetInterfaces()Interfaces
type.GetMethods()PublicMethods(需要时添加 NonPublicMethods)
type.GetProperties()PublicProperties
type.GetFields()PublicFields
type.GetNestedTypes()PublicNestedTypes

模式指南(节选)

首先判断代码是否真的需要兼容裁剪/AOT。普通的 JIT 应用或库无需考虑这些问题——反射在其中完全可用。只有当你或使用者发布裁剪/AOT 版本时,才需要让链接器能够识别反射。

目标绝不是“移除所有反射”,而是让反射在静态分析中可见,以便链接器保留所需内容。请针对每个调用点选择合适的方式:

  • 保留反射并添加注解——最常见的情况。在 Type/string 来源上添加 [DynamicallyAccessedMembers(...)],并沿调用链传递;在入口点标记 [RequiresUnreferencedCode]/[RequiresDynamicCode]。这些属性正是为了让反射在裁剪场景下仍能正常工作。请参阅 trimming-attributes-reference.md。

  • 用编译时替代方案取代反射——当你希望移除反射接口时(例如,不希望公开注册 API 带有 [RequiresUnreferencedCode])。源生成器会生成直接的类型引用(标记属性 → 生成的注册代码);[UnsafeAccessor] 可取代 FieldInfo/MethodInfo/ConstructorInfo。请参阅 aot-trimming-playbook-reference.md 和 aot-trimming-playbook-reference.md。

  • 隔离整个动态路径——当某条路径本质上是动态的(生成代理、按名称加载插件)时,将其置于边界之后,并通过 RuntimeFeature.IsDynamicCodeSupported 选择兼容裁剪的替代方案。请参阅 aot-trimming-playbook-reference.md。

  • 仅在万不得已时抑制警告——在已证明不变量成立的叶节点处添加一个 [UnconditionalSuppressMessage(...)],并提供 Justification。切勿使用 #pragma(它不会保留在 IL 中)。

请参阅 aot-trimming-playbook-reference.md,了解完整的模式指南,其中包括 object 重载 → 泛型重载的重定向、生成的拦截抑制器、有意设置的扫描边界、迁移分析器、严格模式优先级,以及临时警告批准基线。

参考文件

  • trimming-attributes-reference.md:完整的属性目录——RequiresUnreferencedCode、RequiresDynamicCode、DynamicallyAccessedMembers(完整的 DynamicallyAccessedMemberTypes 列表)、UnconditionalSuppressMessage、DynamicDependency——每项都包含用途、精确签名和规则,以及 IL2xxx/IL3xxx 警告代码表。
  • aot-trimming-playbook-reference.md:完整的模式指南——源生成器与反射、兼容裁剪的隔离区和功能保护、拆分安全/不安全代码、System.Text.Json 源生成、使用 [OverloadResolutionPriority] 和选择加入分析器将 object 重载 → 泛型重载重定向、迁移分析器、警告批准基线、已知陷阱,以及完整的生成检查清单。

生成检查清单(摘要)

  1. 项目 — 库设置 IsTrimmable/IsAotCompatible;应用设置 PublishTrimmed/PublishAot;发布时确保零警告。
  2. 显式标注反射(或替换反射) — 为保留的反射添加标注(DynamicallyAccessedMembers/RequiresUnreferencedCode/RequiresDynamicCode);仅在希望彻底移除反射接口时,才用源生成器、泛型(typeof(T))API 或 [UnsafeAccessor] 替换。
  3. 添加标注 — 对每个 Type/string 来源使用范围最窄的 DynamicallyAccessedMembers;沿调用链传递;在不安全入口点添加 [RequiresUnreferencedCode]/[RequiresDynamicCode]。
  4. 隔离 — 将不安全的反射/发出代码放在边界后;使用 RuntimeFeature.IsDynamicCodeSupported 为动态代码路径添加门控,并通过 #if 或标注分离修剪路径。
  5. 禁止警告 — 仅在已证明不变量成立的叶节点使用 [UnconditionalSuppressMessage] 并提供理由;绝不使用 #pragma/SuppressMessage。
  6. 验证 — 发布修剪版和 AOT 版,运行分析器,并使用批准基线锁定警告范围。

参考资料

评论

支持 Markdown