Rust实现COM DLL完整指南FaceWinUnlock-Tauri中DllGetClassObject与引用计数管理原理【免费下载链接】FaceWinUnlock-Tauri一款基于 Tauri 框架开发的现代化 Windows 面容识别解锁增强软件。它通过自定义 Credential Provider (DLL) 注入 Windows 登录界面结合前端 Vue 3 和后端 OpenCV 人脸识别算法为用户提供类似 Windows Hello 的解锁体验。项目地址: https://gitcode.com/gh_mirrors/fa/FaceWinUnlock-TauriFaceWinUnlock-Tauri 是一款基于Tauri框架开发的 Windows 面容识别解锁增强软件它的核心能力是向 Windows 登录界面注入一个自定义凭据提供程序Credential Provider从而实现类似 Windows Hello 的刷脸解锁体验。要完成这项系统级集成关键就是用Rust编写一个符合 COM 规范的动态链接库COM DLLWindows 登录进程通过DllGetClassObject获取类工厂再依靠**引用计数Reference Counting**机制安全地管理 DLL 的加载与卸载。本文将带你完整拆解这一底层原理。COM DLL 与 Windows 凭据提供程序在 Windows 的登录认证体系中登录界面WinLogon/CredUI会枚举注册在注册表中的凭据提供程序。一个凭据提供程序本质上就是一个 COM 对象系统并不知道它的内部实现只认两样东西CLSID一串全局唯一标识符GUID系统用它来点名你的组件。COM 接口ICredentialProvider、ICredentialProviderCredential等标准接口。因此要让系统加载你的解锁逻辑就必须把它打包成一个 COM DLL并实现三个约定好的导出函数DllGetClassObject、DllCanUnloadNow、DllMain。FaceWinUnlock-Tauri COM DLL 自动解锁实现流程架构上图完整呈现了这条链路DLL 加载→DllMain初始化 → 通过SampleClassFactory创建SampleProvider实例 → 管道监听 → 数据接收 → 凭据序列化登录。下面逐一看它的实现。DllGetClassObjectCOM 组件的入口函数DllGetClassObject是 COM DLL 的前台接待。当系统拿着一个 CLSID 来要求创建对象时Windows 就会调用它。它是连接外部系统与内部实现的桥梁。在 lib.rs 中项目首先定义了自己凭据提供程序的唯一身份标识// 定义凭据提供程序的 GUID用于系统识别 pub const CLSID_SampleProvider: GUID GUID::from_u128(0x8a7b9c6d_4e5f_89a0_8b7c_6d5e4f3e2d1c);随后DllGetClassObject负责校验请求并返回类工厂三个入参rclsid、riid、ppv任一为空立即返回E_INVALIDARG避免空指针崩溃比对请求的 CLSID 是否等于CLSID_SampleProvider不匹配则返回CLASS_E_CLASSNOTAVAILABLE匹配成功后把内部的SampleClassFactory转成IClassFactory接口并query给调用方。#[unsafe(no_mangle)] pub unsafe extern system fn DllGetClassObject( rclsid: *const GUID, riid: *const GUID, ppv: *mut *mut c_void, ) - HRESULT { if unsafe { *rclsid } CLSID_SampleProvider { let factory: IClassFactory SampleClassFactory.into(); // query 接口并返回 } else { CLASS_E_CLASSNOTAVAILABLE } }这里的#[unsafe(no_mangle)]和extern system非常关键前者保证函数名不被编译器修饰、能被 Windows 按名字找到后者声明采用 Windows 的stdcall调用约定二者共同确保系统能正确调进你的函数。IClassFactory 类工厂与 CreateInstance 实例化拿到类工厂后系统会调用CreateInstance来真正创建对象。这是 COM 工厂模式的体现——组件不直接 new而是统一经由工厂产出。lib.rs 用 windows-rs 的#[implement]宏生成了IClassFactory的实现拒绝聚合如果调用方传入了外部punkouter对象即要求聚合直接返回CLASS_E_NOAGGREGATION创建凭据提供程序实例化SampleProvider并query出请求的接口。fn CreateInstance(self, punkouter: Ref_, IUnknown, riid: *const GUID, ppv_object: *mut *mut c_void) - Result() { if punkouter.is_some() { return Err(CLASS_E_NOAGGREGATION.into()); } let provider: ICredentialProvider SampleProvider::new().into(); provider.query(riid, ppv_object) }这里的SampleProvider是实现了ICredentialProvider的核心类负责管理登录磁贴Tile的整个生命周期。值得注意的是LockServer方法——它专门用来锁定 / 解锁 DLL与下文引用计数直接联动fn LockServer(self, flock: BOOL) - windows::core::Result() { if flock.as_bool() { dll_add_ref(); } else { dll_release(); } Ok(()) }DllMainDLL 生命周期与初始化DllMain是 DLL 的入口点函数Windows 在加载 / 卸载 DLL 时会以不同原因码调用它。项目利用DLL_PROCESS_ATTACH进程加载 DLL 时完成一次性的环境准备见 lib.rs。pub unsafe extern system fn DllMain(_hinst_dll: HINSTANCE, dw_reason: u32, _reserved: *mut c_void) - BOOL { match dw_reason { DLL_PROCESS_ATTACH { // 从注册表读取日志路径并初始化日志系统 } _ {} } BOOL::from(true) }它在被 WinLogon 进程加载时会读取注册表项SOFTWARE\facewinunlock-tauri中的DLL_LOG_PATH把日志系统初始化好。这一步正是为后续排查DllGetClassObject与引用计数状态服务的——每一次关键调用都会info!落盘。引用计数管理G_REF_COUNT 与 add_ref / release这是全文最核心的部分。COM DLL 与普通库最大的区别就是它不能随时被卸载。因为 DLL 一旦被加载进某个进程比如 WinLogon如果系统贸然FreeLibrary卸载它而此刻还有 COM 对象SampleProvider正存活在内存里就会立刻导致野指针、崩溃甚至蓝屏。解决办法就是经典的引用计数用一个全局计数器记录当前还有多少对象存活只有当计数归零时系统才被允许卸载 DLL。在 lib.rs 中项目用一个原子整数作为全局计数器// 全局引用计数器用于管理 DLL 的生命周期 static G_REF_COUNT: AtomicI32 AtomicI32::new(0); pub fn dll_add_ref() { let new_count G_REF_COUNT.fetch_add(1, Ordering::SeqCst) 1; info!(DLL引用计数增加当前计数: {}, new_count); } pub fn dll_release() { let new_count G_REF_COUNT.fetch_sub(1, Ordering::SeqCst) - 1; info!(DLL引用计数减少当前计数: {}, new_count); }这里有两个工程要点AtomicI32Ordering::SeqCst登录界面是多线程环境Advise注册事件、管道监听线程都可能同时活动用原子操作 顺序一致性保证计数在并发下不乱序、不丢失全程info!打日志把每次增减都记录下来方便在日志里回溯计数为什么没归零这是排查 DLL 无法卸载问题的生命线。计数的加与减分别发生在对象的创建和销毁时刻。看 CSampleProvider.rsimpl SampleProvider { pub fn new() - Self { info!(SampleProvider::new - 创建凭据提供程序实例); dll_add_ref(); // 每创建一个对象计数 1 // ...初始化内部状态... } } // 实现 Drop trait在对象销毁时减少引用计数 impl Drop for SampleProvider { fn drop(mut self) { info!(SampleProvider::drop - 销毁凭据提供程序实例); dll_release(); // 对象被释放计数 -1 } }这里妙处在于利用了 Rust 的Droptrait只要SampleProvider实例被 drop系统调用Release归零后销毁它dll_release就会被自动调用。Rust 的 RAII 机制天然保证了创建与销毁的对称避免了 C 时代容易漏写Release导致的泄漏。DllCanUnloadNow 与 LockServer让 DLL 安全卸载有了计数器就要告诉系统现在能不能卸载。DllCanUnloadNow就是这个门卫#[unsafe(no_mangle)] pub unsafe extern system fn DllCanUnloadNow() - HRESULT { let count G_REF_COUNT.load(Ordering::SeqCst); if count 0 { S_OK } else { S_FALSE } }计数为0→ 返回S_OK系统可以安全FreeLibrary卸载 DLL计数不为 0→ 返回S_FALSE系统会保持 DLL 加载直到最后一个对象被释放。再配合CreateInstance旁边的LockServerflocktrue时加计数、false时减计数就形成了一套完整的生命周期闭环创建对象加计数 → 对象存活期间系统不卸载 → 对象销毁减计数 → 归零后系统自动卸载。项目更新日志中也提到修复面容识别慢时导致 DLL 无法被安全卸载的 Bug正是这条闭环在真实场景中的一次打磨。exports.def声明 DLL 导出符号最后光写函数还不够必须告诉链接器把哪些函数暴露出去。项目用了一个导出定义文件 exports.defLIBRARY winlogon EXPORTS DllGetClassObject PRIVATE DllCanUnloadNow PRIVATE DllMain PRIVATELIBRARY winlogon指定了生成 DLL 的文件名即winlogon.dll风格的凭据提供程序命名三个导出函数全部标记为PRIVATE确保只有本 DLL 内部可见的符号被显式暴露。编译时需配合 Rust 的crate-type [cdylib]与 MSVC 工具链需安装 Visual Studio 的 C 桌面开发组件生成最终的 COM DLL。部署完成后系统会在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{8a7b9c6d-4e5f-89a0-8b7c-6d5e4f3e2d1c}下注册该 CLSID登录进程据此找到你的组件。下图是软件在部署阶段对 WinLogon 进程所做修改的风险告知界面FaceWinUnlock-Tauri 部署时 WinLogon 凭据提供程序注册配置警告小结一次完整的 COM 生命周期把整条链路串起来你就掌握了用 Rust 实现 COM DLL 的完整套路DllMain在进程加载时初始化日志等基础环境系统凭 CLSID 调用DllGetClassObject校验并返回IClassFactory工厂的CreateInstance生成SampleProvider同时dll_add_ref计数 1对象存活期间DllCanUnloadNow始终返回S_FALSE阻止卸载系统Release对象 → RustDrop触发dll_release计数 -1计数归零后DllCanUnloadNow返回S_OKDLL 被安全卸载。⚠️安全提示本项目涉及修改系统注册表与C:\Windows\System32目录属于系统级操作。强烈建议在虚拟机VMware / Hyper-V中调试并参考 Server/README.md 中的部署与恢复方案避免误操作导致无法正常登录。通过这套DllGetClassObject 原子引用计数 Drop自动回收的组合FaceWinUnlock-Tauri 把 COM 组件最容易踩坑的生命周期管理用 Rust 的安全语义优雅地兜住了——这正是它在 Windows 登录界面稳定运行的底层保障。【免费下载链接】FaceWinUnlock-Tauri一款基于 Tauri 框架开发的现代化 Windows 面容识别解锁增强软件。它通过自定义 Credential Provider (DLL) 注入 Windows 登录界面结合前端 Vue 3 和后端 OpenCV 人脸识别算法为用户提供类似 Windows Hello 的解锁体验。项目地址: https://gitcode.com/gh_mirrors/fa/FaceWinUnlock-Tauri创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考