硬边界:COM 互操作的限制
所有功能基本都跑通了但有一条硬边界碰上了——COM 互操作。这不只是我们项目遇到的问题。微软官方的 Native AOT 限制列表里明确把 “Windows: No built-in COM” 列在第一条。这个限制的覆盖面比很多人想象的更广——剪贴板、拖放、RichEdit 控件、Shell 接口等底层都涉及 COM。TDS 的 winform 版本里受影响的场景是在文件列表上右键弹出 Windows 系统原生的 Shell 上下文菜单资源管理器里的打开方式/发送到/属性菜单。核心调用涉及 Marshal.GetTypedObjectForIUnknown——把非托管 IUnknown 指针包装成托管 RCWRuntime Callable Wrapper的方法var pUnknown …; // 从 Shell API 获取的 IUnknown 指针var shellFolder Marshal.GetTypedObjectForIUnknown(pUnknown, typeof(IShellFolder));这条路径在 AOT 下无法工作的原因有两层RCW 的动态生成——AOT 编译器在编译期无法确定最终会传入什么 IID、什么接口因此不能预生成对应的包装代码。Trim 移除元数据——typeof(IShellFolder) 对应的元数据在 trim 阶段可能被视作没有托管代码直接调用而移除。尽管部分第三方库试图通过 ComWrappers 机制绕过内置 COM 路径可以为 winform 提供有限的 AOT COM 支持但即便是最新版仍有大部分 API 仍然可能无法正常使用。因此目前还是放弃系统原生 Shell 菜单支持了aot编译下强行打开系统菜单程序会直接崩溃。六、结论与展望回到开头的那个问题winform 能用 Native AOT 吗如果只看结果——我们在 TDS 项目中完成了 dotnet publish -r win-x64 -c Release 并拿到了一个可独立运行的 exe绝大多数核心功能正常工作——那么答案偏乐观。但仔细拆开来看这是一个比较微妙的状态winform 团队在 dotnet/winform#4649 里长期推进 trimming 兼容工作到 .NET 10 时已有部分路径清理完毕。SDK 层面的 NETSDK1175 错误主要起保护作用加上 _SuppresswinformTrimError 后确实可编译。社区里有人形容它是挡板而非铁门。COM 相关的限制Marshal.GetTypedObjectForIUnknown、BuiltInComInterop是 AOT 自身的硬边界跟 winform 本身无关。所以更准确的表述是在 .NET 10 下不涉及 COM 动态互操作的中等复杂度 winform 项目通过合理配置和少量代码调整可以成功产出可运行的 AOT exe。但它不是官方推荐的发布方式_SuppresswinformTrimError 是一个内部逃逸通道而非公开 API不保证所有路径都安全。如果项目重度依赖COM 互操作尤其是动态 IID 解析和 Marshal.GetTypedObjectForIUnknownComponentResourceManager / .resx 反射式资源加载DataGridView 数据绑定反射枚举属性路径会被 trim 移除RichTextBox底层 RichEdit COM 控件包装大量 System.Reflection 调用那 AOT 适配的坑会显著增多建议在评估清楚之前不要轻易上生产。但反过来看对于小型工具类应用——快速截屏、文件批量重命名、剪贴板历史管理、系统托盘监控程序、简单数据录入——winform AOT 的组合确实很有吸引力零运行时依赖exe 发出去就能跑用户不用装 .NET启动接近瞬间AOT 省去了 JIT 编译时间体积可接受实测简单 winform AOT exe 约 10–20 MB两个我们没有用到的高频高风险控件DataGridView社区反馈在 AOT 下容易出现列类型解析失败的问题因为内部的 DataGridViewColumnType 映射依赖反射枚举所有加载程序集的类型。RichTextBox底层的 RichEdit COM 控件包装存在 RCW 生成问题与第五节描述的 COM 限制同一类。如果在用这两个控件AOT 适配的难度会更大——但也不是完全不可能只是需要更深的拦截和替换。以后想做一个 Windows 小工具又不想让用户装运行时、也不想用 C 或其他依赖重新造一遍 UI 轮子——熟悉的WinForm AOT 这个组合值得放进你的工具箱里。配置改三行代码改几处出来的就是一个孤零零的 exe跑在用户机器上又快又省心。七、最后