47 KiB
AtomUI AOT 编程规范
这份文档给日常写 AtomUI 代码的人用。它不是 AOT 改造记录,而是以后新增控件、主题、图标、语言资源、Gallery 示例和发布配置时要遵守的规则。linked publish 的系统架构、模式矩阵、Registration Unit 和安全 fallback 由 AOT 与裁剪架构统一定义,Sidecar 和静态计划由 AOT Linked Registration Pipeline定义,本文不复制其长期契约。
目标很简单:
src/里的库项目在 AOT、trim、single-file analyzer 下不产生项目自身 warning。- Gallery 需要 NativeAOT 发布时,可以通过 analyzer,也可以完成真实 publish。
- AOT 改造不能改变原来的控件行为、绑定语义、异常语义、资源释放边界和关键性能路径。
先看这几条
日常开发先记住这 7 条,绝大多数 AOT 问题都能在写代码时避开。
- 新增功能和修复 bug 时,AOT 兼容是第一设计约束。同一需求有 AOT 友好实现和运行时反射/动态发现实现时,必须选择 AOT 友好实现;能用 source generator 就不要用反射。
- 不要在 AtomUI 内置路径里新增字符串绑定,例如
new Binding("Name")或 AXAMLReflectionBinding。 - 不要运行时扫描 assembly、type、field、property 来完成内置注册。能显式注册就显式注册,能生成 registry/catalog 就用 source generator。
- 不要用
UnconditionalSuppressMessage盖掉 trim/AOT warning。它只是不显示 warning,不会保留被 trim 掉的 metadata。 - 替换 AOT 不安全代码时,先确认旧语义,再改实现。尤其是 binding mode、binding priority、初始值、异常包装、dispose 后行为。
- 新增 subscription、binding、event handler、activation scope、cache 时,必须能说清楚在哪里释放或失效。
- Analyzer 通过不等于 NativeAOT publish 一定成功。涉及发布配置、linker、root descriptor 时,要做真实 publish 验证。
一句话总结:AOT 改造的方向是把运行时动态发现变成编译期已知代码,而不是把 warning 压下去。
按场景查
| 场景 | 推荐写法 | 避免写法 | Review 重点 |
|---|---|---|---|
| 控件属性同步 | AvaloniaProperty、GetObservable、BindUtils.RelayBind(...) |
new Binding("Path") |
mode、priority、初始值、dispose 后行为 |
| 模板内 part 同步 | C# 里拿到 template part 后强类型绑定 | AXAML ReflectionBinding |
template reapply 时旧 part 是否释放 |
| 内置类型注册 | source generator 生成 registry/catalog | Assembly.GetTypes() |
generator 和生成物是否一致 |
| token converter | generator 生成静态数组 | 运行时扫描 attribute 后 Activator.CreateInstance |
数量、顺序、map 行为是否不变 |
| 语言资源 | generated Catalog descriptor + compiled Translation Bundle | GetFields(...) 枚举资源字段 |
Catalog ID、占位符和回退语义是否不变 |
| 图标创建 | generated factory 或 virtual factory | 扫描 icon assembly 后反射创建 | 非法 kind 的异常包装是否不变 |
| DataGrid 动态 path | [GenerateDataMemberAccessors] 或手写 descriptor |
对用户模型直接 GetProperty(path) |
sort/filter/group/AddNew 是否走 descriptor |
| 非 Visual AvaloniaObject 资源宿主 | [GenerateScopedResourceHost] 生成 scoped host 生命周期 |
每个对象手写 IResourceHost / IThemeVariantHost 样板代码 |
owner attach/release、WeakReference、资源更新测试 |
| ReactiveUI view activation | AtomUI/Gallery 自己管理 activation scope | view-side WhenActivated extension 反射路径 |
Loaded/Unloaded 和 VM 切换释放 |
| 发布配置 | analyzer 加真实 NativeAOT publish | 只看普通 build | linker、root、generator 项目是否被错误发布 |
适用范围
这份规范覆盖以下项目:
src/AtomUI.Coresrc/AtomUI.Controls.Sharedsrc/AtomUI.Controlssrc/AtomUI.Desktop.Controlssrc/AtomUI.Desktop.Controls.DataGridsrc/AtomUI.Desktop.Controls.ColorPickersrc/AtomUI.Desktop.Controls.Extrassrc/AtomUI.Icons.*src/AtomUI.Generatorcontrolgallery/AtomUIGallerycontrolgallery/AtomUIGallery.Desktop
Binding
为什么字符串 binding 有风险
new Binding("Name")、ReflectionBinding 这类写法把成员访问延迟到运行时:运行时拿到字符串,再去找属性、字段或索引器。NativeAOT 和 trimming 会删除静态代码没有证明会用到的 metadata,所以这类路径很容易出现 analyzer warning,严重时运行时才失败。
AtomUI 内置控件和 Gallery AOT 路径应尽量把绑定改成编译期可见的属性访问。
AvaloniaObject 之间同步属性
两个 Avalonia object 之间同步属性时,优先使用 AvaloniaProperty:
disposables.Add(BindUtils.RelayBind(
source,
SourceControl.SomeProperty,
target,
TargetControl.SomeProperty,
BindingMode.OneWay,
BindingPriority.Template));
BindUtils.RelayBind(...) 的语义要求:
OneWay:从 source 的GetObservable(sourceProperty)绑定到 target。TwoWay:保留 source-to-target 和 target-to-source 的双向同步,不能把 target 初始值错误写回 source。OneWayToSource:只把 target 后续变化写回 source,不能额外制造 source-to-target 行为。OneTime:也要保持旧 binding 的写入 priority 和释放语义,不能简单写成SetCurrentValue(...) + Disposable.Empty,除非旧语义本来就是一次性写入且无需恢复。
不要再新增字符串 path overload。BindUtils.RelayBind(object, string, ...) 这类 API 应保持为兼容边界或编译期错误入口。
非 AvaloniaObject 数据源
对 INotifyPropertyChanged 类型,可以用属性名过滤通知,但取值必须走强类型 getter:
disposables.Add(BindUtils.RelayBind(
viewModel,
nameof(MyViewModel.Options),
static vm => vm.Options,
optionsControl,
SomeControl.OptionsProperty));
这里的 nameof(...) 只用于判断是哪一个属性变了,不允许再用反射按名字读取属性值。
Template part binding
模板里不要新增 ReflectionBinding。template part 之间要同步时,在控件类里拿到 part 后绑定:
_templateBindingDisposables?.Dispose();
_templateBindingDisposables = new CompositeDisposable();
if (partA is not null && partB is not null)
{
partB.Bind(
TargetProperty,
partA.GetObservable(SourceProperty),
BindingPriority.LocalValue)
.DisposeWith(_templateBindingDisposables);
}
这里最容易出错的是生命周期:
OnApplyTemplate开始时先释放旧的 disposable。- template reapply 后,旧 part 不能继续被 observable、event handler 或 binding 持有。
- 删除旧 binding 前,要确认旧 binding 是否会跟随动态 effective value 变化。不能因为初始默认值一样就删掉。
Visual ancestor binding
需要找 visual ancestor 时,使用 BindUtils.BindVisualAncestor(...):
_relayBindingDisposables.Add(BindUtils.BindVisualAncestor(
this,
target,
TargetControl.OpenOnProperty,
typeof(TopLevel),
priority: BindingPriority.Template));
注意:
ancestorLevel从 1 开始。- 返回的 disposable 必须在 detach 或 template reapply 时释放。
- 不要用一次性的
TopLevel.GetTopLevel(this)替代动态 ancestor binding,除非旧语义就是只查一次。
Source Generator
什么时候该用 SG
如果代码需要做这些事,优先考虑 source generator:
- 扫描类型并注册。
- 根据 attribute 生成 registry。
- 根据 enum 创建具体类。
- 把资源字段写入 dictionary。
- 根据数据模型生成属性 accessor。
- 为 closed generic 或具体类型生成 factory。
- 为 owner-managed 非 Visual
AvaloniaObject生成 scopedIResourceHost/IThemeVariantHost生命周期样板代码。
SG 的价值不是“把反射挪个地方”,而是让运行时代码变成普通的强类型 C#。这样 trimmer 能看见类型、构造函数和成员,NativeAOT 也不需要动态代码生成。
非 Visual AvaloniaObject 资源宿主类需求统一遵循 Scoped Resource Host 开发规范。不要在每个描述对象中复制手写资源宿主代码;业务属性保留在主文件,资源宿主生命周期由 generator 生成,owner 控件只负责 attach/release。
Generator 项目边界
AtomUI.Generator 是 Roslyn analyzer 项目,不是运行时依赖。引用方式保持:
<ProjectReference Include="../AtomUI.Generator/AtomUI.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
生成文件可以输出到 GeneratedFiles/,但项目要继续排除重复编译;这些编译产物默认不跟踪:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>GeneratedFiles</CompilerGeneratedFilesOutputPath>
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs"/>
PublishAot=true 是 MSBuild global property,会沿着 ProjectReference 传播。generator 是 netstandard2.0 analyzer 项目,不应该参与 NativeAOT 发布,所以 generator 项目要把这些属性隔离在本项目内:
TreatAsLocalProperty="IsAotCompatible;EnableAotAnalyzer;EnableTrimAnalyzer;EnableSingleFileAnalyzer;PublishAot;PublishTrimmed;PublishSingleFile;RunAOTCompilation;SelfContained;RuntimeIdentifier"
并且在 generator 项目里显式关闭运行时发布属性。
第一方用户不需要为了 AOT 或 trimming 额外引用 AtomUI.Generator。声明 AtomUIRegistrationPackageId 的产品包会在
NuGet 中内嵌同版本 Generator、Build Tasks 和 buildTransitive assets。现有应用保留显式 Generator PackageReference
仍然受支持,但最终 @(Analyzer) 中只能有一份 AtomUI.Generator.dll。
维护这条打包链时遵守以下约束:
- Generator 和 Build Tasks 只能进入 NuGet 的编译期目录,不能进入
lib/、普通输出或 publish 目录。 - 多个产品包的入口必须幂等;Analyzer 去重依据
ResolveReferences后的最终编译器输入。 - MSBuild 项目求值阶段的
Import、ItemGroup或 item Condition 不得引用 item list;需要检查@(Analyzer)时放入BeforeTargets="CoreCompile"的 Target。 - Release 打包必须先用相同
AtomUIVersion构建 Generator/Build Tasks;修改版本后不能用--no-build复用旧输出。 - 验证至少覆盖只引用一个产品包、多个产品包并存、保留显式 Generator 引用、普通非裁剪构建和真实 NativeAOT publish。
生成物必须稳定
改 generator 时要同时看两层:
- writer 代码是不是符合预期。
GeneratedFiles/AtomUI.Generator/**里的生成物是不是能由当前 writer 稳定复现。
不能只手改生成物,也不能只改 generator 却不检查生成物 diff。之前 review 里已经出现过 generator writer 和提交的生成物不一致,这类问题会让后续维护很难判断真实来源。
包级生成代码统一位于 AtomUI.Generated.<AssemblyOwner>。AssemblyOwner 必须是由程序集名生成的单一 PascalCase
标识符,点号、连字符、下划线和其他非字母数字字符只作为单词边界,不进入最终标识符。例如:
AtomUI.Controls -> AtomUI.Generated.AtomUIControls
AtomUI.Desktop.Controls -> AtomUI.Generated.AtomUIDesktopControls
AtomUI.Desktop.Controls.DataGrid -> AtomUI.Generated.AtomUIDesktopControlsDataGrid
Source Generator、Localization writer、Linked Registration metadata 和 AXAML Theme wrapper Build Task 必须复用同一
命名 helper,不得各自维护 sanitizer。手写代码引用生成入口时必须引用当前项目自己的 owner namespace;禁止依赖
InternalsVisibleTo 从其他 AtomUI 包调用同名 Generated* 类型,因为这种错误可能正常编译却注册错误 Package。
Linked registration
PublishTrimmed=true、PublishAot=true 和 WebAssembly RunAOTCompilation=true 使用同一套生成式 Registration Unit
计划。Control descriptor、Own Token schema、内部控件和控件族专属 Theme Asset factory 必须能聚合为完整 Unit;不得新增
全包静态数组、全资产 switch 或“构造全集后过滤”的 linked 路径。
linked analysis 必须位于独立 Analyzer assembly。普通 Debug 和未启用 AOT/Trim 的 Release 不得把该 Analyzer 传给 csc,
也不得运行 AXAML usage、Sidecar 或 Application Plan target。仅在 Generator callback 中快速 return 不算满足零成本要求。
Control Package 默认使用 Package 粒度:
<AtomUIRegistrationGranularity>Package</AtomUIRegistrationGranularity>
该属性可以省略,省略即为 Package。整个包生成一个安全 Unit,普通第三方包、DataGrid、ColorPicker、Extras 和
GalleryBase 都使用该模式。包作者不声明 AtomUIRegistrationUnit,也不维护 Unit dependency。
只有包含大量独立控件族、并且已经建立真实 linked publish 和体积回归验证的包才允许显式设置:
<AtomUIRegistrationGranularity>Directory</AtomUIRegistrationGranularity>
当前第一方只有 AtomUI.Desktop.Controls 使用 Directory 模式。启用 Directory 后,控件族目录才成为 Unit 边界;
Presenter、Cell、View、Semantic Part 和基础主题仍跟随所属控件族,不能继续按内部目录细分。
Package 注册入口的正式契约见
AOT 与裁剪架构。新增或迁移 Control Package 的
真实 UseXxxControls() 方法必须使用
[ControlPackageRegistrationEntry] 声明入口。Generator 从 IMethodSymbol 派生 manifest method identity,并在 Package
自身编译阶段验证扩展方法签名、可见性和重载集合。禁止在 .csproj、props、targets 或 NuGet metadata 中手写入口类型名
和方法名,也禁止使用方法命名约定或方法体扫描猜测入口。
AtomUIRegistrationPackageId 只表示 Package identity 和产品包 build asset 注入边界。它不能携带注册方法信息,也不能作为
是否存在源码入口的替代判断。没有 [ControlPackageRegistrationEntry] 的程序集不得输出 linked Package、Unit、ControlMap
或 full fragment metadata,但仍可以生成普通非裁剪路径使用的 full registration helper。
Language Catalog、内置 Translation Bundle、Dialog/Tooltip/Motion/Responsive 初始化、Global Token、Theme Algorithm、
Provider 和平台 selector 属于 Package Core,不为它们创建细粒度 fragment。只有必须在未选择 Control Unit 时仍随入口
加载,或在 Directory 模式下确实跨多个 Unit 的共享主题资源,才通过 AtomUIPackageSharedTheme 显式声明;owner 解析
失败不得自动归类为共享资源。Package 模式下的普通 Control Theme 不需要该 metadata。
普通 AXAML/C# 使用由 Generator 自动发现。AtomUIRegistrationUnitRoot 和 AtomUIPackageRoot 只用于类型字符串、Loose
AXAML、动态插件等编译期无法确定的场景,不能成为普通 Control 接入步骤。无法可靠确定 Unit 时应在编译期只把对应
Package 扩大为 full fallback 并给出诊断,不能依赖运行时反射或 late registration 修补。
Directory 模式的 Package Theme 如果直接实例化另一个 AtomUI Control,该元素必须能通过 using:、
clr-namespace: 或当前程序集 XmlnsDefinition 精确解析,Generator 会把同 Package Unit 的直接依赖写入 Sidecar UnitEdge。
C# 中可证明的跨 Unit Control 使用也必须形成直接 UnitEdge。不要依赖短类型名猜测,也不要把模板元素依赖写进 Theme Asset descriptor 的
referenced identities;后者会改变普通非裁剪 schema 和 fingerprint。无法证明 owner 或依赖时必须 full fallback。
Unit fragment 必须是叶子,只注册本 Unit 的 descriptor、Theme asset 和 resource factory。禁止生成 AddDependencies、调用
其他 Unit 或调用 TryEnterUnit。应用 Generator 在编译期对 UnitEdge 计算 closure/SCC,并让每个 fragment 最多出现一次。
依赖分析使用候选驱动 Incremental API。禁止对所有 SyntaxTree 执行 DescendantNodes(),禁止从 invocation 递归进入 callee
body,也禁止为不同 owner Unit 重复扫描相同方法、属性或字段。分析必须有确定性结构预算;超限时当前 Package full fallback,
不能使用墙钟超时产生非确定输出。
ControlMap 是 CLR Control ownership,不是 descriptor 清单。定义程序集里的 public、非泛型 Control 即使没有 Theme
descriptor,也要归入 Registration Unit 并拥有 ControlMap;只有原本可主题化的 Control 才能进入 builder.AddControl。
引用程序集不得重复输出 ControlMap。纯基础设施程序集没有 [ControlPackageRegistrationEntry] 时,不应生成
Package、Unit、ControlMap 或 full fragment metadata。
AtomUI.Controls Common 层始终由 Desktop 完整注册,不是独立 linked Package。不要为 UseCommonControls() 添加入口
Attribute,不要为了让 Common 参与应用计划而复制 UseDesktopControls() 的方法 identity,也不要引入跨 Package Unit 闭包。
类库只在被 linked 应用作为 ProjectReference 构建或执行 NuGet Pack 时生成 Usage Sidecar。普通 Debug/Release 必须跳过 usage
分析和 Sidecar 生成。应用在项目文件内设置 PublishAot、PublishTrimmed 或 RunAOTCompilation 后,AtomUI targets 自动把
AtomUILinkedPublish=true、AtomUIRegistrationPlanOwner=false 沿直接和传递 ProjectReference 递归传播;不要要求用户补传内部
属性,也不要把 PublishAot、RID、SelfContained 传播给类库。analyzer 类型 ProjectReference 必须排除。
如果 ProjectReference companion 仍缺失,consumer 从 package producer assembly 的 metadata 提取清单;结果带
ExtractedManifest,相关 Package full fallback 且不产生诊断。对于不能随图重编译的普通预编译 DLL,只扫描直接引用已知
control package assembly 的候选,通过 IL call/callvirt/ldftn/ldvirtftn 恢复 entry,并为相关 Package 产生
PackageRoot 与 ExtractedConsumerAssembly。恢复路径始终 full fallback;只有 PackageRoot 没有 Entry 时必须用
ATOMUILINK008 报告消费 DLL。缺失或无法验证的 Sidecar 不能解释为没有 usage。
ATOMUILINK002、ATOMUILINK007 和 ATOMUILINK010 只在 linked publish 或显式 AtomUIRegistrationStrict=true 验证中显示;strict 模式用于
CI 把自动 full fallback 或未覆盖的动态创建提升为 error。ATOMUILINK010 不触发 full fallback,只提示用显式 root 覆盖。
Sidecar 来源和重复防护
维护 linked-registration 构建资产时,必须遵守以下不变量:
- 一个程序集在一次 linked build 中只能有一个生效的 Sidecar。
- ProjectReference companion 与 NuGet package 的顺序只用于选择同 hash 等价候选;metadata extraction 只在正式 Sidecar 缺失时使用。
- 不能用“DLL 旁边没有 Sidecar”证明“构建中没有 Sidecar”;NuGet 正式 Sidecar 通常位于
buildTransitive。 - 收集阶段必须先解析
assembly.name和contractHash,再按程序集身份去重,最后才注入AdditionalFiles。 - 同身份同 hash 可以折叠为一份;同身份不同 hash 必须失败并报告来源,不能随机保留第一份。
- 已有正式 Package/companion Sidecar 的程序集禁止再次生成
ExtractedManifest;普通 ProjectReference 缺失 companion 时仍须保留 extraction fallback。 ExtractedConsumerAssembly只用于没有正式 Sidecar 的预编译消费 DLL;它必须同时经过最终 canonical resolution,不能覆盖或 合并掉正式 consumer Sidecar。
任何新增 Sidecar、consumer target、Pack asset 或 extraction 逻辑,都必须同时验证 NuGet、ProjectReference、混合引用和重复传递
包场景。不要在 Generator 中吞掉重复声明,也不要通过关闭 extraction、修改文件名或 suppress ATOMUILINK005 掩盖来源冲突。
修改 Generator ABI、Sidecar schema、feature switch 或 Public fragment entry point 时,按 Public API 和协议 review,并运行 普通构建零 linked-analysis、trimmed JIT、NativeAOT 和非裁剪兼容验证。系统契约见 AOT Linked Registration Pipeline。
只有 Directory 模式的内部 resource-only Theme 无法按目录推导到正确 Unit 时,才使用 AtomUIRegistrationUnit
metadata 明确归属,不要把它升级为 AtomUIPackageSharedTheme。Package 模式不得添加这类 ownership 修补。抽象 typed
theme 没有 generated resource wrapper 时,不得生成虚假的 wrapper 调用;应由同 Unit 的具体资源静态保留。
第三方 Control Package 检查
普通第三方 Control Package 的 AOT 接入必须保持简单:
- 声明稳定的
AtomUIRegistrationPackageId。 - 引用兼容版本的 AtomUI 产品包,复用其内嵌的同版本 Generator 和构建资产;普通包不重复添加 Generator 引用。
- 在真实 public
UseXxxControls()扩展方法上添加[ControlPackageRegistrationEntry]。 - 保持 full/generated 注册分支,以及 Provider、Localization 和 initializer 的顺序。
- 使用默认 Package 粒度,不写 Unit ownership、Unit dependency 或 linker XML。
- 至少验证 ordinary 与 generated registration 行为快照;发布 AOT 兼容声明前跑真实 trimmed JIT 和 NativeAOT。
操作示例见 第三方 AtomUI Control Package 指南。 粒度和资源归属的正式规则见 AOT Registration Unit 粒度。
AOT/Trim 注册命名
AOT/Trim 注册是发布基础设施,不是通用运行时 feature。AtomUI.Core 中跨程序集使用的隐藏 ABI 统一位于
AtomUI.Registration 命名空间,并采用以下命名:
AotTrimRegistration
AotTrimControlPackageRegistrationBuilder
AotTrimRegistrationPlan
AotTrimRegistrationPlanRegistry
AotTrimRegistration.IsEnabled 只读取发布期标记;它不负责执行裁剪、发现控件或安装注册计划。对应的全局
AppContext key 为 AtomUI.AotTrimRegistration.Enabled。类型名不重复 AtomUI,因为命名空间已经提供产品边界;
AppContext key 则必须保留 AtomUI 前缀,因为它是进程级字符串协议。
GeneratedApplicationRegistrationPlan、Generated*UnitFragment 等 Generated* 名称仅限 Generator 生成的内部输出,
不要把它们作为运行时 ABI 的通用命名。Generator 内部协议类型可以使用 LinkedRegistration*,Build Task 使用
CollectAxamlUsageTask、WriteLinkedRegistrationSidecarTask、ValidateApplicationRegistrationPlanTask 这类动作导向名称。
Theme / Token
Control 与 Token 注册
运行时不要扫描 assembly 查找可主题化 Control 或 Control Token。应由 generator 生成
ControlTokenDescriptorPool,为每个对外可主题化 Control 返回完整 descriptor;没有 Own Token 的 Control 也必须
拥有 identity 和 descriptor:
descriptors.Add(MyControlTokenDescriptor.Instance);
descriptor 必须直接提供以下静态已知信息:
- exact Control CLR type、
ControlTokenIdentity和 registry slot。 - 无参数
[ControlDesignToken]标记的可选 Own Token 类型;Attribute 不携带 Control 类型或 identity。 - 可选的 Own Token builder 直接构造委托。
- Own Token name、value type、stage 和 slot。
- 强类型 parse、set、get 和 resource projection 委托。
OwnTokensschema;Control 的可配置 Global Token 集合始终是完整 Global Token schema,不生成消费白名单。- ControlTheme asset owner、引用的 Control identities、Semantic Part 契约和包级注册信息。
Semantic Part descriptor 必须由 Control 声明和构建输入静态生成。运行时不得扫描 AXAML、ControlTheme、
Classes 或 VisualTree 来发现公共 Part,也不得使用 Dictionary<string, Style>、反射 Property 查找或动态 Theme
factory 合并 Part 样式。.semantic-* marker 只参与 Avalonia 原生 Selector;Gallery 的 VisualTree 高亮属于开发
工具路径,不能进入 Control 运行时。
这里有三个关键点:
- Builder 必须原样传递 descriptor,不能丢弃 identity 后退化为
Type注册。 - 内置正常路径不调用
Activator.CreateInstance、Type.GetProperties或PropertyInfo.GetValue/SetValue。 - 第三方 Control 包必须使用 AtomUI generator,并只通过一个真实的包级入口调用生成 registration helper,注册 Control、 可选 Own Token 和主题资产;不提供手写 descriptor、手工 manifest 或反射 fallback 旁路。
- Own Token 可以放在包内正常源码位置并使用
[ControlDesignToken]标记;禁止泛型 Control 参数和手写 ID。 - Control Token 定义继承只允许 Generator 在编译期把显式标记的抽象层扁平化到
sealed终端。抽象层不生成 identity、descriptor、Registration Unit 或动态 root,运行时不扫描或遍历 Token 基类。完整契约见 Control Design Token 继承架构。
Token value converter 注册
不要在运行时通过 Assembly.GetTypes()、IsDefined(...)、Activator.CreateInstance(...) 扫描 converter。应由 TokenValueConverterRegistrationGenerator 生成静态注册:
ITokenValueConverter[] valueConverters =
[
new StringTokenValueConverter(),
new IntegerTokenValueConverter(),
new DoubleTokenValueConverter()
];
改这里时要确认:
- converter 实例数量和旧逻辑一致。
TargetType()到 converter 的 map 行为不变。- 新增 converter 后能在
TokenValueConverterRegistry.g.cs里看到生成结果。
Resource key cache
Resource key cache 应该基于显式 token metadata。不要在资源生成或 theme 切换的 hot path 里反射查找 token property。
cache 要满足:
- key 稳定。
- theme/token 更新时失效边界清楚。
- 不引入无上限增长的缓存。
Language
Catalog 与翻译表走生成代码
所有应用、控件和类库使用 [LanguageCatalog] enum 与 XLIFF 2.1。LocalizationGenerator 在编译期直接生成:
- enum 数字 ID 到 slot 的静态映射。
LanguageCatalogDescriptor和编译后TranslationBundleDescriptor。- 强类型
XxxLangResourceExtension。 GeneratedLanguageModuleRegistration和最终应用 bootstrap。
正常路径禁止通过 Assembly.GetTypes()、GetFields()、Enum.GetNames() 或 Attribute 反射发现 Catalog,也禁止
运行时解析 XLIFF。模块主包的 en-US 与静态语言包目标 XLIFF 只作为 AdditionalFiles 进入最终应用编译;运行时
只保留不可变 Snapshot 和字符串表。
Review 时要看:
- Catalog enum 是否公开、使用稳定显式正整数 ID,且不含别名或
[Flags]。 - 所有 Catalog 是否具有完整
en-US,目标语言的 unit/占位符契约是否一致。 - 类库包级入口是否直接调用生成的模块注册,不扫描程序集。
- 应用 bootstrap 是否只直接注册静态语言包和 Override Bundle。
- 发布目录是否没有 XLIFF、Build Tasks 或 Generator 程序集。
Icon
图标创建走 generated factory
图标 provider 不要运行时扫描 icon assembly,也不要用 Activator.CreateInstance(iconType) 创建图标。生成器负责生成 GetIconType(kind) 和 CreateIcon(kind)。
GetIcon(kind) 的异常包装语义要保留:
try
{
return CreateIcon(kind);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Create icon {kind} failed", ex);
}
这不是细节。非法 kind、构造失败、内部异常的外层异常类型和 message 都属于对外行为。
Gallery icon catalog
Gallery 展示图标时使用 generated catalog:
AntDesignIconCatalog.GetIcons()
catalog entry 应包含:
NameThemeTypeKindIconType- direct
Func<Icon>creator
Gallery 不要运行时扫描 icon assembly。
同类型 icon clone
需要创建同类型 icon 时,不要通过反射构造。使用虚方法:
public override Icon CreateInstance()
{
return new SameIconType();
}
要求:
- 每个生成图标 override 后返回同一具体类型。
- base
Icon.CreateInstance()默认失败,避免静默创建错误类型。 - clone 后只复制旧逻辑复制过的属性,不能额外覆盖 brush、animation、stroke 等状态。
DataGrid / Collection 动态数据
问题本质
DataGrid、collection sort/filter/group、自动生成列这些场景经常拿到用户数据模型和字符串 property path。这里的动态性是功能需求,不能简单把 warning suppress 掉。
正确方向是:对我们能控制的模型生成 accessor;对用户模型提供显式 descriptor;最后才保留带 warning 的兼容 fallback。
推荐写法
可控制的数据模型加 [GenerateDataMemberAccessors]:
[GenerateDataMemberAccessors]
public partial class PersonRow
{
public string? Name { get; set; }
public int Age { get; set; }
}
如果集合以接口或基类作为 item type 暴露,并且排序、过滤或分组 path 来自这个接口/基类,也要在对应接口或基类上生成 accessor;不要依赖运行时从首个 item 反推具体类型。
不可加 attribute 的模型,显式传入 IDataMemberAccessorDescriptor:
var descriptor = new DataMemberAccessorDescriptor<PersonRow>(
static () => new PersonRow(),
new IDataMemberAccessor[]
{
new DataMemberAccessor<PersonRow, string?>("Name", static row => row.Name),
new DataMemberAccessor<PersonRow, int>("Age", static row => row.Age)
});
相关逻辑优先从 descriptor 取信息:
- sort
- filter
- group
- AddNew
- auto-generate columns
- data type inference
- read-only metadata
如果 descriptor 不存在,可以进入 RUC fallback,但调用点必须显式看到风险。RequiresUnreferencedCode 不会保留成员,它只会把风险传递给调用方,让 analyzer 在 AOT/trim 场景下报警。
所以结论是:AtomUI 内置模型要走生成 accessor;用户如果要 NativeAOT 稳定发布,就要提供 generated 或手写 descriptor。
编译期诊断
DataGrid、List、collection view 等使用字符串 path 做排序、过滤、分组、自动列或数据成员读取时,必须优先让问题在编译期暴露,而不是等到 NativeAOT 运行时才失败。
可静态判断的场景必须提供 analyzer warning。诊断 ID、ID 命名、severity、编码组织和测试规则统一维护在 compiler-diagnostics-guidelines.md。
Reflection helper
反射 helper 只能存在于明确边界:
- public compatibility API。
- 用户动态模型 fallback。
- 平台或框架能力探测。
保留反射时要写清楚风险:
- 需要成员 metadata 的
Type参数或返回值,要用DynamicallyAccessedMembers标注。 - 无法证明 trim 安全的 API,要用
RequiresUnreferencedCode标注。 - helper 内不要动态构造泛型类型。优先扫描对象已经实现的 closed interface。
- 不要用
UnconditionalSuppressMessage让调用方误以为代码安全。
可以这样理解:
- DAM 用来告诉 trimmer:“这个 Type 值流到这里时,需要保留哪些成员。”
- RUC 用来告诉调用者:“这个 API 在 trim/AOT 下不保证安全。”
- suppression 只是不显示 warning,不解决问题。
ReactiveUI
AtomUI runtime controls
AtomUI runtime 控件不能依赖 ReactiveUI view-side reflection activation path。ReactiveWindow<TViewModel> 这类基类要自己完成关键行为:
- 实现
IViewFor<TViewModel>。 - 维护
ViewModel和DataContext同步。 Loaded时激活当前IActivatableViewModel。Unloaded或切换ViewModel时释放旧 activation disposable。- 处理 reentrancy,避免 unload 过程中误激活新 VM。
构造函数里空的 this.WhenActivated(...) 没有业务价值,应删除。非空 activation block 要迁移到 AOT 友好的本地 activation API。
Gallery controls
Gallery AOT 路径使用 GalleryReactiveUserControl<TViewModel> 替代 ReactiveUserControl<TViewModel>。
它要保留 ReactiveUI 的核心使用体验:
IViewFor<TViewModel>。ViewModel和DataContext双向同步。- Loaded/Unloaded 对应 view activation scope。
IActivatableViewModel.Activator.Activate()。WhenActivated(Action<CompositeDisposable>)的注册、激活、卸载释放语义。
也就是说,我们不是放弃 ReactiveUI,而是避开它 view-side extension API 中对 AOT 不友好的表达式/反射路径。
Gallery binding / command
Gallery code-behind 不要新增 ReactiveUI expression binding:
this.OneWayBind(...);
this.BindCommand(...);
this.WhenAnyValue(...);
observable.ToProperty(...);
替代写法:
GalleryBindingUtils.OneWay(...)GalleryBindingUtils.BindCommand(...)- setter 内显式更新派生属性
- 显式
BehaviorSubject<T>或IObservable<T>管理 command canExecute
GalleryBindingUtils.BindCommand dispose 时要恢复绑定前 command,避免 activation scope 结束后留下旧 command。
Publish / trimming / NativeAOT
日常构建不运行发布分析器
普通 Debug 和非 AOT、非裁剪的 Release 构建必须关闭 SDK 的 trim、AOT 和 single-file analyzer,避免把发布期静态分析成本带入日常开发。Gallery Desktop 的 Release 项目只保留 IsTrimmable=true 和 IsAotCompatible=true 兼容性声明;PublishTrimmed、PublishAot 和自包含发布属性由发布脚本显式传入,不能隐式改变普通构建的 analyzer 开关。
仓库构建按以下输入自动启用对应 analyzer:
PublishTrimmed=true:启用 trim analyzer。PublishAot=true或RunAOTCompilation=true:启用 trim 和 AOT analyzer。PublishSingleFile=true:启用 single-file analyzer。- 显式传入
EnableTrimAnalyzer、EnableAotAnalyzer或EnableSingleFileAnalyzer时,保留调用方选择,用于专项验证。
ordinary Generator 可以继续生成普通构建所需的 Theme/Token、full registrar 和 leaf Unit fragment,但不能运行应用 usage 分析。
Package/Usage Sidecar 只在 NuGet Pack 或 linked ProjectReference 构建时生成;Application Plan 只能在真实 AOT/Trim linked build
中生成。普通构建必须从 Csc Analyzer item、AXAML target 和 obj 中同时看不到 linked analysis。
Analyzer 和真实 publish 都要跑
AOT/trim analyzer 通过,只说明静态分析没有发现项目自身 warning。它不等于 trimmed JIT 或 NativeAOT 链接和运行一定成功。涉及 Registration Unit、Package fallback、发布配置或 native 依赖时,要做对应模式的真实 publish。
Windows 11 上 Gallery Desktop 的 NativeAOT 工具链、发布命令、产物验证和排障记录见 Windows NativeAOT 发布。Linux 平台的对应手册见 Linux NativeAOT 发布。
库项目 analyzer:
dotnet build src/AtomUI.Core/AtomUI.Core.csproj -c Release --no-incremental \
/p:IsAotCompatible=true \
/p:EnableTrimAnalyzer=true \
/p:EnableAotAnalyzer=true \
/p:EnableSingleFileAnalyzer=true \
/nr:false --nologo -v:minimal
Gallery analyzer:
dotnet build controlgallery/AtomUIGallery/AtomUIGallery.csproj -c Release --no-incremental /m:1 \
/p:IsAotCompatible=true \
/p:EnableTrimAnalyzer=true \
/p:EnableAotAnalyzer=true \
/p:EnableSingleFileAnalyzer=true \
/nr:false --nologo -v:minimal
Gallery NativeAOT publish:
dotnet publish controlgallery/AtomUIGallery.Desktop/AtomUIGallery.Desktop.csproj \
-c Release -r osx-arm64 -p:GalleryPublishTrimmed=true \
-p:GalleryPublishAot=true --self-contained true \
--nologo -v:minimal
Linked registration 完整回归与体积门槛:
scripts/verification/verify-aot-trim-registration.sh --full
脚本成功仍不能代替 Gallery Desktop 启动 smoke。Theme template 可以静态保留 CLR 类型而漏注册它的 descriptor;这类错误 只有窗口模板应用和首帧布局实际运行时才会暴露。发布后至少确认进程稳定进入主窗口,无 active theme schema、资源加载或 initializer 异常,再主动终止 smoke 进程。
macOS NativeAOT
当前 macOS 本机发布使用:
<ItemGroup Condition="'$(PublishAot)' == 'true' and $([MSBuild]::IsOSPlatform('OSX'))">
<LinkerArg Include="-L/opt/homebrew/lib"
Condition="Exists('/opt/homebrew/lib/libbrotlienc.dylib')"/>
<LinkerArg Include="-L/opt/homebrew/opt/openssl@3/lib"
Condition="Exists('/opt/homebrew/opt/openssl@3/lib/libssl.dylib')"/>
<LinkerArg Include="-L/usr/local/lib"
Condition="Exists('/usr/local/lib/libbrotlienc.dylib')"/>
<LinkerArg Include="-L/usr/local/opt/openssl@3/lib"
Condition="Exists('/usr/local/opt/openssl@3/lib/libssl.dylib')"/>
</ItemGroup>
原因:
- NativeAOT 链接
System.Net.Security.Native时需要libssl/libcrypto。 - NativeAOT 链接
System.IO.Compression.Native时还需要 Brotli 原生库。 - Apple Silicon Homebrew 的通用库目录通常是
/opt/homebrew/lib,openssl@3keg-only 库位于/opt/homebrew/opt/openssl@3/lib;Intel Homebrew 对应/usr/local路径。默认 linker 搜索路径可能找不到这些库。 - 仓库内 macOS 验证统一复用
build/MacOSHomebrewNativeAot.targets,不要在 Gallery 或 fixture 中重复硬编码路径。 - 该文件只补充 Homebrew OpenSSL/Brotli linker 搜索路径,不进入 NuGet,也不是 AtomUI 的通用 NativeAOT 配置。 Windows 和 Linux 使用共享 AOT 配置及各自平台工具链,不需要空的对称 targets 文件。
注意:
- 默认不要添加
-ld_classic。删除粗粒度 root 后,当前 NativeAOT 产物已经可以用默认 Apple linker 完成链接。 - 如果某个 Xcode/NativeAOT 组合再次触发
too many large addends,先验证是否是环境问题,再作为本机或 CI 发布参数处理,不要默认固化到项目文件里。 - 如果 CI 或开发机不是 Homebrew ARM64 路径,需要在发布脚本里提供正确的 linker search path。
- NativeAOT publish 输出的是裸 Mach-O 可执行文件。macOS 图形应用要双击或通过 LaunchServices 启动,需要
.appbundle。
Roots.xml
Roots.xml 不能保留解析不到的 assembly:
<assembly fullname="Some.Old.Assembly" preserve="All"/>
无效 root 只会产生 IL2007 warning,不会保留任何东西。删除 stale root 不改变运行时行为。
新增 root 前要说明:
- 为什么不能通过静态引用或 generator 保留。
- preserve 范围为什么不能更小。
- 是否会明显扩大 NativeAOT 体积。
AtomUI linked registration 的 AtomUIRegistrationUnitRoot 和 AtomUIPackageRoot 是生成器语义 root,不是 linker XML
的替代写法。它们只用于应用动态输入,并由 Application Registration Plan 展开为强类型 Unit 调用或 Package full
fallback;不要把包级 preserve="All" 搬进 Roots.xml 来绕过 Manifest、Unit 归属或 fallback 缺陷。
Browser WebAssembly 发布
Browser 发布要先区分两类问题:
- runtime 在
dotnet.create()/mono_wasm_load_runtime阶段失败:优先怀疑 SDK、workload、runtime pack、WebAssembly 输出格式或浏览器兼容性。 - managed
Main已进入后失败:再回到 AtomUI、Gallery 业务代码和资源加载路径。
不要在 runtime 初始化阶段的错误上盲改业务代码。先用最小纯 .NET Browser 项目验证,再用最小 Avalonia Browser 项目验证,最后才回到 Gallery。
当前 AtomUIGallery.Browser 普通发布固定关闭 Webcil:
<WasmEnableWebcil>false</WasmEnableWebcil>
原因是当前 .NET 10 browser-wasm 工具链生成的 Webcil managed assembly 在 Chromium 下会在 mono_wasm_load_runtime 阶段失败;同一个最小纯 .NET Browser 项目关闭 Webcil 后可以正常进入 managed Main。这个配置只改变 managed assembly 的发布包装格式,不改变 AtomUI/Gallery 的运行语义。
后续升级 .NET SDK、wasm-tools workload 或 runtime pack 时,可以用临时最小 Browser 项目重新做单变量验证。普通发布的关键对照是:
dotnet publish path/to/DotNetBrowserSmoke.csproj \
-c Release -p:RunAOTCompilation=false \
--nologo -v:minimal
dotnet publish path/to/DotNetBrowserSmoke.csproj \
-c Release -p:RunAOTCompilation=false -p:WasmEnableWebcil=false \
--nologo -v:minimal
只有默认 Webcil 输出也能稳定启动时,才能考虑删除 WasmEnableWebcil=false。
当前 Browser AOT 结论
截至 2026-06-11,在下面这套环境里,AtomUIGallery.Browser 的 WebAssembly AOT 结论是:
| 发布方式 | 结果 | 说明 |
|---|---|---|
RunAOTCompilation=false + WasmEnableWebcil=false |
可以发布,也可以运行 | Gallery Browser 可以进入 Avalonia canvas,页面显示加载完成。 |
RunAOTCompilation=true + WasmEnableWebcil=false |
可以发布,但不能运行 | 浏览器启动阶段在 mono_wasm_load_runtime 失败,错误是 RuntimeError: remainder by zero,未进入 managed Main。 |
也就是说,WasmEnableWebcil=false 只解决普通 Browser 发布的 Webcil 加载问题;它不能解决当前环境下 WebAssembly AOT runtime 初始化失败的问题。
本次验证环境:
| 项 | 版本 |
|---|---|
| OS | macOS 26.3.1(a), build 25D771280a, arm64;dotnet --info 识别为 Mac OS X 26.3 |
| .NET SDK | 10.0.300 |
global.json SDK |
10.0.300, rollForward=latestFeature |
| .NET host runtime | 10.0.8, osx-arm64 |
| MSBuild | 18.6.3+caa81fa49 |
| workload set | 10.0.301.1 |
| wasm-tools manifest | 10.0.109/10.0.100 |
Microsoft.NETCore.App.Runtime.Mono.browser-wasm pack |
10.0.9 |
Emscripten 3.1.56 SDK / Node / Cache packs |
10.0.9 |
| Avalonia / Avalonia.Browser | 12.0.4 |
| ReactiveUI.Avalonia | 12.0.3 |
| ReactiveUI | 23.2.28 |
| SkiaSharp native WebAssembly assets | 3.119.4 |
| HarfBuzzSharp native WebAssembly assets | 8.3.1.3 |
| 浏览器 | Codex in-app Browser;当前插件环境未能读取具体 UA 版本 |
本次产物规模:
| 发布方式 | 发布目录 | dotnet.native*.wasm |
|---|---|---|
| 普通发布,关闭 Webcil | 约 105 MB | 约 9.3 MB |
| AOT 发布,关闭 Webcil | 约 240 MB | 约 111 MB |
当前工程决策:
- Gallery Browser 发布保持
RunAOTCompilation=false。 AtomUIGallery.Browser保留WasmEnableWebcil=false,用于保证普通 Browser 发布可运行。- 当前不要把 Browser AOT 作为可交付发布目标;只有在 .NET SDK、wasm-tools、runtime pack、Avalonia Browser 或浏览器版本升级后,重新验证
RunAOTCompilation=true能稳定启动,才能打开。 - 如果重新验证 Browser AOT,先跑最小纯 .NET Browser AOT,再跑最小 Avalonia Browser AOT,最后跑 Gallery Browser AOT。runtime 初始化阶段失败时,不要先改 AtomUI/Gallery 业务代码。
Review 时看什么
每个 AOT 改动 review 时,至少回答这些问题:
- 旧实现依赖了哪种 AOT 不安全机制。
- 新实现如何消除这个机制。
- 旧语义是否保持,包括 binding priority、初始值、异常包装、排序、过滤、缓存顺序等。
- 新增 disposable、event handler、binding、activation scope 在哪里释放。
- 是否引入额外 per-instance 成本或 hot path 成本。
- generator 和生成物是否一致。
- analyzer 和必要测试是否通过。
- 是否需要真实 NativeAOT publish 验证。
Review 进度维护在:
docs/superpowers/aot-review-checklist.md
每通过一个 review 点,要立即更新状态。
新增代码自查
新增或修改 AtomUI 代码时,先扫一遍:
- 是否新增
new Binding("...")、AXAMLReflectionBinding或字符串 path binding。 - 是否新增 assembly/type/member 扫描。
- 是否新增
Activator.CreateInstance(Type)、Expression.Compile()或MakeGenericType(...)。 - 是否新增 ReactiveUI expression/view activation API。
- 是否新增动态 data model path,但没有 descriptor 或 generator。
- 是否新增 DataGrid/List/collection 字符串 path,但没有可在编译期报警的 analyzer 覆盖。
- 是否新增订阅、binding、event handler,但没有 release path。
- 是否新增 source generator 逻辑,但没有检查生成物稳定性。
- 是否新增 suppress trim/AOT warning。
- 是否修改 publish/linker/root descriptor,但没有真实 publish 验证。
建议 grep:
rg -n "new Binding\\(|ReflectionBinding|Assembly\\.GetTypes|GetFields\\(|GetCustomAttribute|Activator\\.CreateInstance|Expression\\.Compile|MakeGenericType|WhenAnyValue|ToProperty|OneWayBind|BindCommand|ReactiveUserControl" src controlgallery -g '*.cs' -g '*.axaml'
grep 命中不一定都是错误,但每个命中都要能说明边界和原因。
推荐验证命令
常规源码构建:
dotnet build AtomUI.slnx -c Release --no-incremental /m:1 /nr:false --nologo -v:minimal
全解决方案 AOT analyzer:
dotnet build AtomUI.slnx -c Release --no-incremental /m:1 \
/p:IsAotCompatible=true \
/p:EnableTrimAnalyzer=true \
/p:EnableAotAnalyzer=true \
/p:EnableSingleFileAnalyzer=true \
/nr:false --nologo -v:minimal
Gallery NativeAOT publish:
dotnet publish controlgallery/AtomUIGallery.Desktop/AtomUIGallery.Desktop.csproj \
-c Release -r osx-arm64 -p:GalleryPublishTrimmed=true \
-p:GalleryPublishAot=true --self-contained true \
--nologo -v:minimal
Focused tests:
dotnet test tests/AtomUI.Desktop.Controls.Tests/AtomUI.Desktop.Controls.Tests.csproj --nologo -v:minimal /m:1 /nr:false
dotnet test tests/AtomUI.Controls.Shared.Tests/AtomUI.Controls.Shared.Tests.csproj --nologo -v:minimal /m:1 /nr:false
dotnet test tests/AtomUI.Desktop.Controls.DataGrid.Tests/AtomUI.Desktop.Controls.DataGrid.Tests.csproj --nologo -v:minimal /m:1 /nr:false
dotnet test tests/AtomUIGallery.Tests/AtomUIGallery.Tests.csproj --nologo -v:minimal /m:1 /nr:false
Diff hygiene:
git diff --check
已知受控边界
下面这些命中不等于必须删除,但必须维持注解和文档边界:
TypeHelper动态 path fallback。ObjectExtension/TypeMemberExtension反射 helper。- DataGrid 对用户
Binding/ReflectionBinding的兼容读取。
共同要求是:AtomUI 内置正常路径不用这些 fallback;用户动态场景使用时风险要显式暴露;AOT 用户要有 descriptor、generator 或显式注册这样的稳定替代路径。