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-Tauri
FaceWinUnlock-Tauri 是一款基于Tauri框架开发的 Windows 面容识别解锁增强软件,它的核心能力是向 Windows 登录界面注入一个自定义凭据提供程序(Credential Provider),从而实现类似 Windows Hello 的刷脸解锁体验。要完成这项系统级集成,关键就是用Rust编写一个符合 COM 规范的动态链接库(COM DLL):Windows 登录进程通过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实例 → 管道监听 → 数据接收 → 凭据序列化登录。下面逐一看它的实现。
DllGetClassObject:COM 组件的入口函数
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(()) }DllMain:DLL 生命周期与初始化
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); }这里有两个工程要点:
AtomicI32+Ordering::SeqCst:登录界面是多线程环境(Advise注册事件、管道监听线程都可能同时活动),用原子操作 + 顺序一致性保证计数在并发下不乱序、不丢失;- 全程
info!打日志:把每次增减都记录下来,方便在日志里回溯"计数为什么没归零",这是排查 DLL 无法卸载问题的生命线。
计数的"加"与"减"分别发生在对象的创建和销毁时刻。看 CSampleProvider.rs:
impl 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旁边的LockServer(flock=true时加计数、false时减计数),就形成了一套完整的生命周期闭环:创建对象加计数 → 对象存活期间系统不卸载 → 对象销毁减计数 → 归零后系统自动卸载。项目更新日志中也提到"修复面容识别慢时导致 DLL 无法被安全卸载的 Bug",正是这条闭环在真实场景中的一次打磨。
exports.def:声明 DLL 导出符号
最后,光写函数还不够,必须告诉链接器"把哪些函数暴露出去"。项目用了一个导出定义文件 exports.def:
LIBRARY "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_OK,DLL 被安全卸载。
⚠️安全提示:本项目涉及修改系统注册表与
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),仅供参考