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,用户只能看任务管理器叹气。
防御清单
- 用
Weak<ComPtr<T>>或手动raw_this()裸指针打破循环; - 对可能缓存的 COM 对象实现
final_release(见 C++/WinRT 模式),在最后一次Release里把清理任务抛给后台队列,避免在Drop里再做查询; - 给每个 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