Hotdry.
systems-engineering

用 Rust 写 Outlook 插件:COM 互操作踩坑与内存安全实战

Outlook 32 位进程地址空间翻倍后,Rust COM 加载项如何避开高地址误判、引用计数循环与 4-Crash 拉黑机制。

Outlook 2021/2016 的 32 位进程在 64 位 Windows 上默认开启「大地址感知」(LAA),可用地址空间从 2 GB 直接翻倍到 4 GB。对 C++ 老插件来说,这只是多了一点 “呼吸空间”;但对刚用 windows-rs 写的 Rust COM 加载项,却可能一脚踩进三个深坑:高地址误判、引用计数循环、Office 自动拉黑。本文把我们在实战里流的血,总结成可落地的参数与代码片段,帮你把插件稳定跑到下一次 Office 更新。

坑 1:高地址误判 → 越界写 → 随机崩溃

LAA 之后,HeapAlloc 返回 0x8xxxxxxx 以上地址再正常不过。老代码里若有一句:

if (ptr > 0x7FFFFFFF) { /* 认为是内核地址,直接跳过 */ }

在 2 GB 时代 “永远为真” 的保护,现在会误把合法堆块当 “非法指针”—— 轻则逻辑跳过,重则 memcpy 越界,Outlook 直接重启。Rust 侧更隐蔽:

let p = heap_alloc(len) as usize;
if p > 0x7FFF_FFFF { return Err("invalid"); } // ❌

检测 / 修复:启动阶段做一次地址空间探测,把阈值写成运行时配置:

fn probe_max_user_address() -> usize {
    use windows::Win32::System::Memory::*;
    unsafe {
        let mut info = MEMORY_BASIC_INFORMATION::default();
        VirtualQuery(usize::MAX as _, &mut info, size_of_val(&info));
        info.BaseAddress as usize + info.RegionSize - 1
    }
}

返回 0x7FFE_FFFF(2 GB)或 0xFFFE_FFFF(4 GB)即知 LAA 是否生效,后续逻辑用 probe_max_user_address() 做边界,而非硬编码 0x7FFFFFFF。

坑 2:引用计数循环 → 内存泄漏

windows-rs 把 IUnknown 的三件套封装成 ComPtr<T>,看起来 “自动 Release”,但遇到「双向持有」就抓瞎:

struct Manager { cache: ComPtr<IFolder> }
struct Folder { mgr: ComPtr<IManager> }

Outlook 会缓存插件对象,循环引用导致计数永不为 0,几十次操作后进程内存飙到 1 GB,用户只能看任务管理器叹气。

防御清单

  1. Weak<ComPtr<T>> 或手动 raw_this() 裸指针打破循环;
  2. 对可能缓存的 COM 对象实现 final_release(见 C++/WinRT 模式),在最后一次 Release 里把清理任务抛给后台队列,避免在 Drop 里再做查询;
  3. 给每个 Rust 对象加 drop_count: AtomicU32,单元测试里断言进程退出前归零 —— 泄漏一次就挂 CI。

代码模板:

impl Drop for MyComObject {
    fn drop(&mut self) {
        // 仅做日志,不做 QueryInterface!
        log::trace!("MyComObject dropped");
    }
}
unsafe extern "system" fn final_release(this: *mut c_void) {
    let boxed = Box::from_raw(this as *mut MyComObject);
    tokio::spawn(async move {
        // 异步清理,不阻塞 STA
        drop(boxed);
    });
}

坑 3:4-Crash 规则 + 内存阈值 → 插件被永久拉黑

Office 默认策略:

  • 同一会话内崩溃 ≥ 4 次,COM 加载项被自动禁用;
  • 物理内存占用 > 80 % 时,每 5 s 扫描,单个插件内存 > 50 % 即弹警告并建议禁用。

Rust 侧 “安全” 并不等于 “省内存”—— 一次 Vec<u16> 收邮件全文就可能 50 MB,再触发一次重新分配,直接触碰红线。

可运行参数

  • 把大容量缓存拆成 mmap 文件,内存映射按页失效;
  • 邮件正文流式处理,单封不超过 512 KB 缓冲区;
  • #[global_allocator] 接管 Rust 堆,统计并上报 HeapSize;当 Office 内存告警事件(IDispatch 接口 OnWarning)到达,立即调用 SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1) 主动 Trim,实测可把内存占比从 55 % 打到 20 % 以下,告警消失

一键核查表(CI & 上线前)

检查项 命令 / 代码 通过标准
地址空间探测 probe_max_user_address() 返回 0xFFFE_FFFF(4 GB)时逻辑正确
循环引用 drop_count.load(SeqCst) == 0 进程退出前归零
泄漏测试 cargo test --features leak-sanitizer definitely lost
Release 配对 !heap -p -h 0(WinDbg) IUnknown 残留
4-Crash 模拟 注册表 RestartManagerRetryLimit=3 人工崩溃 3 次 第 4 次仍能被加载(说明崩溃计数已重置)
内存告警 OnWarning 事件后内存占比 < 40 % 进程管理器截图留档

小结

Rust 的内存安全保证的是「不悬垂、不双杀」,但 COM 的引用计数模型与 Office 的运行时策略把「泄漏」和「崩溃」的定义往前又推了一步:高地址误判、循环强引用、进程级内存红线,哪一个踩中都会被用户一键拉黑。把地址探测、final_release、异步清理、内存 Trim 做成标配模板,才能让插件在 4 GB 时代既跑得欢,又活得久。


参考资料
[1] Microsoft Support, 《Outlook 中的大地址感知》, 2025-11
[2] CSDN, 《windows-rs 内存泄漏排查:Rust 安全机制下的调试技巧》, 2025-09

查看归档