☰
Rust实现COM DLL完整指南:FaceWinUnlock-Tauri中DllGetClassObject与引用计数管理原理
2026/10/9 3:06:43 网站建设 项目流程

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 PRIVATE

LIBRARY "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 的完整套路:

  1. DllMain在进程加载时初始化日志等基础环境;
  2. 系统凭 CLSID 调用DllGetClassObject校验并返回IClassFactory;
  3. 工厂的CreateInstance生成SampleProvider,同时dll_add_ref计数 +1;
  4. 对象存活期间,DllCanUnloadNow始终返回S_FALSE阻止卸载;
  5. 系统Release对象 → RustDrop触发dll_release计数 -1;
  6. 计数归零后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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询