通用工具库(utils):加密、哈希、图像、进程与节流
src/utils/ 是 SpinningMomo 的横切基础设施层,以纯函数与轻量状态对象的形式为业务模块提供加密摘要、流式哈希、图像/图形处理辅助、系统进程查询以及调用节流等通用能力。
Purpose and Scope
本页覆盖 src/utils/ 中与目录标题直接相关的五个子域的架构与实现细节:
- 加密(
utils/crypto):基于文件路径的 SHA-256 摘要计算。 - 哈希(
utils/hash):基于 XXH3 的流式/区间采样哈希,用于媒体指纹。 - 节流(
utils/throttle):Leading/Trailing Edge 语义的线程安全调用节流器。 - 进程与系统(
utils/system):进程权限级别查询等 Windows 系统能力。 - 图像(
utils/image):图像辅助工具入口。
同时说明整个 utils 层的统一架构约定(std::expected 错误传播、vendor/std.hpp 统一头、命名空间划分)。
以下内容有意留给兄弟页面,本页仅列出模块归属而不深入:
- 屏幕捕获与 D3D/HDR 图形管线(
utils/graphics):见 graphics 捕获相关页面。 - 显示器几何与 DPI(
utils/display)、对话框(utils/dialog)、文件/MIME(utils/file)、崩溃转储(utils/crash_dump)、日志(utils/logger)、媒体编码与音频(utils/media)。
Overview
src/utils/ 目录按"一目录一模块、一模块一命名空间"的方式组织,每个模块通常只包含一对 name.hpp / name.cpp 文件(模板与头文件实现为主时甚至没有 .cpp,例如 hash/xxhash.hpp 与 throttle/throttle.hpp)。从仓库实际文件可见的模块划分如下:
| 模块目录 | 命名空间 | 职责 |
|---|---|---|
src/utils/crypto/ | utils::crypto | SHA-256 文件摘要 |
src/utils/hash/ | utils::hash | XXH3 流式哈希与区间采样指纹 |
src/utils/throttle/ | utils::throttle | 泛型调用节流状态机 |
src/utils/system/ | (system) | 进程权限级别等系统查询 |
src/utils/image/ | (image) | 图像辅助工具 |
src/utils/graphics/ | — | capture / capture_region / d3d / hdr / photo_processing(兄弟页面) |
src/utils/display/ | — | display / display_geometry(兄弟页面) |
src/utils/file/ | — | file / mime(兄弟页面) |
src/utils/media/ | — | audio_capture / encoder / hdr_convert / state / types(兄弟页面) |
src/utils/dialog/ crash_dump/ logger/ | — | 对话框、崩溃转储、日志(兄弟页面) |
关键架构约定(从已读源码可归纳):
- 错误处理统一为
std::expected<T, std::string>:所有可能失败的工具函数都返回std::expected,错误类型为字符串描述(如"Hash calculation cancelled"、"Input stream read failed"),调用方用if (!result)/.error()消费,不使用异常。 - 统一 vendor 标准库封装头:每个模块都
#include "vendor/std.hpp",将标准库依赖集中收敛到一处,便于切换编译器/C++ 标准配置。 - C++20/23 特性大量使用:
std::stop_token(协作式取消)、std::span、std::format、[[nodiscard]]、CTAD 友好的模板特化(ThrottleState<void>)。 - RAII 资源绑定:C 资源用
std::unique_ptr+ 删除器绑定(如StatePtr = std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)>),保证提前返回路径也能释放。 - 尾置返回类型 +
auto:全库统一auto fn(...) -> Ret风格。
Architecture
下面是本页覆盖的五个子域及其依赖关系(实线为"调用/包含"依赖,数据库形节点为外部依赖):
设计意图解读:
- utils 层不依赖业务层,只依赖
vendor/与标准库/OS API,保证可被任意业务模块安全引用而不会形成环。 - hash 模块是纯算法模块(头文件实现),不持有状态、不开线程,协作式取消通过
std::stop_token参数化注入——由调用方(后台任务框架)决定何时停止。 - throttle 模块是状态模块,用
std::unique_ptr<ThrottleState>持有可变状态,天然支持"一个状态对象被多线程共享"的场景。 - crypto 与 system 是薄封装模块:只暴露
sha256_file()、is_process_elevated()这类单一职责函数,把 Win32/CNG 的样板代码封在.cpp中(src/utils/crypto/crypto.cpp、src/utils/system/system.cpp)。
Core Flow:流式哈希的端到端执行路径
hash_stream_to_hex 是 utils 层最典型的"资源受限 + 可取消"算法流程。它把任意大小的输入流按 1 MiB 分块送入 XXH3 流式状态,内存占用与文件总大小无关:
关键点(对应源码注释的设计意图):
- 停止延迟有界:取消检查发生在每块读取之前,因此退出延迟最多受"当前正在进行的这一次同步读取"影响,不会等到整个文件读完。
- EOF 前先提交已读数据:
gcount() > 0时即使已置 EOF 也要先update,避免丢失最后一块。 - badbit/failbit 区分语义:
bad()表示底层读取失败;fail()在非 EOF 情况下同样视为失败——两者都不能把已读到的部分内容当成完整文件。 - 空文件语义:
!has_data返回unexpected("Input stream is empty"),保持"空文件不生成可用媒体哈希"的业务约定。
加密与哈希实现详解
utils::crypto::sha256_file
唯一公开入口,签名来自头文件:
1namespace utils::crypto {
2
3// 计算文件 SHA-256(小写十六进制字符串)
4auto sha256_file(const std::filesystem::path& file_path) -> std::expected<std::string, std::string>;
5
6} // namespace utils::cryptoSource: crypto.hpp
设计要点:入参是 std::filesystem::path(而非字符串),由调用方负责编码;返回小写十六进制字符串,与常见校验和工具输出一致;失败路径返回 std::expected<std::string, std::string> 错误信息。实现体在 src/utils/crypto/crypto.cpp(本页源码预算内未读取,故不展开其内部调用序列)。
utils::hash:XXH3 流式会话三件套
1constexpr std::size_t kReadBufferSize = 1024 * 1024;
2
3struct StreamRange {
4 std::uint64_t offset = 0;
5 std::size_t size = 0;
6};
7
8using StatePtr = std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)>;
9
10// 创建并初始化一次 XXH3 流式会话
11inline auto create_state() -> std::expected<StatePtr, std::string> {
12 auto state = StatePtr(XXH3_createState(), &XXH3_freeState);
13 if (!state) {
14 return std::unexpected("Failed to create XXH3 state");
15 }
16
17 if (XXH3_64bits_reset(state.get()) != XXH_OK) {
18 return std::unexpected("Failed to reset XXH3 state");
19 }
20
21 return state;
22}Source: xxhash.hpp
为什么用 unique_ptr + XXH3_freeState 删除器:XXH3 是 C 库,XXH3_state_t 由 XXH3_createState() 分配、必须由 XXH3_freeState() 释放。用 RAII 别名 StatePtr 包装后,所有提前返回(unexpected)路径都会自动释放资源,消除泄漏风险——这是 utils 层"C 资源 RAII 化"的统一手法。
会话三件套是组合式 API,而不是一个大函数:
| 函数 | 职责 |
|---|---|
create_state() | 建立并 reset 一次 XXH3 会话 |
update_state(state, data, size) | 追加一段内存(size == 0 直接返回 {},跳过无效调用) |
digest_state(state) | std::format("{:016x}", XXH3_64bits_digest(state)) 输出 16 位十六进制 |
拆成三件套的原因:让 hash_stream_ranges_to_hex(采样哈希)能复用同一会话先喂元数据、再喂多个区间,而不必复制读取循环逻辑。
区间采样指纹 hash_stream_ranges_to_hex
1inline auto hash_stream_ranges_to_hex(std::istream& stream, std::span<const std::byte> metadata,
2 std::span<const StreamRange> ranges,
3 std::stop_token stop_token)
4 -> std::expected<std::string, std::string> {
5 auto state_result = create_state();
6 if (!state_result) {
7 return std::unexpected(state_result.error());
8 }
9 auto state = std::move(state_result.value());
10
11 // 元数据使用固定宽度二进制字段,避免字符串拼接产生边界歧义。
12 auto metadata_result = update_state(state.get(), metadata.data(), metadata.size_bytes());
13 if (!metadata_result) {
14 return std::unexpected(metadata_result.error());
15 }
16 ...Source: xxhash.hpp
设计意图(来自源码注释):对大文件只哈希"固定元数据 + 若干采样区间",用 StreamRange{offset, size} 描述每个采样段。元数据刻意用固定宽度二进制字段而非字符串拼接,防止 "ab"+"c" 与 "a"+"bc" 这类拼接歧义导致不同输入产生相同指纹(哈希碰撞放大)。每个区间开始前与区间内循环中都检查 stop_token,seek 失败返回 "Input stream seek failed"。
节流(throttle):Leading/Trailing Edge 状态机
utils::throttle 解决"高频事件(如 UI 回调、60fps 定时器)只允许以固定间隔真正执行一次"的问题。它不是简单丢弃溢出调用,而是缓存最后一次被跳过的参数(Trailing Edge),保证最终状态不丢失。
状态模型
1// 节流状态(带参数版本)
2template <typename... Args>
3struct ThrottleState {
4 std::chrono::milliseconds interval{16}; // 节流间隔,默认约60fps
5 std::chrono::steady_clock::time_point last_call_time; // 上次执行时间
6 bool has_pending{false}; // 是否有待处理的调用
7 std::tuple<Args...> pending_args; // 待处理的参数
8 std::mutex mutex; // 线程安全
9};
10
11// 节流状态(无参数特化版本)
12template <>
13struct ThrottleState<void> {
14 std::chrono::milliseconds interval{16};
15 std::chrono::steady_clock::time_point last_call_time;
16 bool has_pending{false};
17 std::mutex mutex;
18};Source: throttle.hpp
注意 ThrottleState<void> 是 ThrottleState<Args...> 的显式特化,去掉 pending_args 成员,避免为无参回调携带冗余 tuple。默认 interval{16}(≈60fps)表明该模块的主要预期消费场景是渲染/UI 频率控制。
核心调用逻辑 call
1template <typename Func, typename... Args>
2inline auto call(ThrottleState<Args...>& state, Func&& func, Args... args) -> bool {
3 std::lock_guard lock(state.mutex);
4
5 auto now = std::chrono::steady_clock::now();
6 auto elapsed = now - state.last_call_time;
7
8 if (elapsed >= state.interval) {
9 // 满足间隔,立即执行
10 state.last_call_time = now;
11 state.has_pending = false;
12 std::forward<Func>(func)(args...);
13 return true;
14 } else {
15 // 间隔不足,缓存参数(Leading Edge executed, Trailing Edge scheduled)
16 state.has_pending = true;
17 state.pending_args = std::make_tuple(args...);
18 return false;
19 }
20}Source: throttle.hpp
语义:首次调用立即执行(last_call_time 在 create() 时被置为 epoch,允许立即首调),间隔不足时不执行但覆盖缓存参数。返回 true 表示本次已执行,false 表示被节流跳过且参数已缓存——调用方据此决定是否需要后续 flush。用 steady_clock 而非 system_clock 是刻意的:节流判定不应受系统时间跳变(NTP 校时)影响。
Trailing Edge 补发 flush
1template <typename Func, typename... Args>
2inline auto flush(ThrottleState<Args...>& state, Func&& func) -> bool {
3 std::lock_guard lock(state.mutex);
4
5 if (!state.has_pending) {
6 return false;
7 }
8
9 // 执行缓存的参数
10 std::apply(std::forward<Func>(func), state.pending_args);
11
12 state.has_pending = false;
13 state.last_call_time = std::chrono::steady_clock::now();
14 return true;
15}Source: throttle.hpp
flush 用 std::apply 解包缓存的 tuple 并执行,同时刷新 last_call_time——避免 flush 之后立即又被 call 命中执行,破坏节流节奏。典型用法:事件风暴结束时(如窗口 resize 结束、指针离开)调用 flush,保证最终一次状态被落地。
状态转移图
进程与系统(utils/system)
该模块暴露 Windows 系统级查询,本页源码预算内确认的唯一公开签名:
// 检测当前进程是否以管理员权限运行
[[nodiscard]] auto is_process_elevated() noexcept -> bool;Source: system.hpp
设计要点:[[nodiscard]] 强制调用方消费返回值(查询函数的返回值被忽略几乎必然是 bug);noexcept 承诺该查询不会抛异常——这类 Win32 包装函数失败时通常返回 false 而非抛出,保证可在构造/析构等 noexcept 场景安全调用。典型用途是按权限级别调整 UI 提示或功能可用性(具体调用点未在本次源码预算内检索)。
图像与图像模块边界
src/utils/image/image.hpp 是图像辅助工具入口(本页源码预算内未展开其内容)。需要区分它与兄弟目录的职责:
utils/image/:通用图像辅助(本页范围)。utils/graphics/:capture、capture_region、d3d、hdr、photo_processing——屏幕捕获、D3D 设备管理与 HDR 管线,属图形捕获子系统的兄弟页面。utils/media/:audio_capture、encoder、hdr_convert、state、types——录制/编码子系统,兄弟页面。
Usage Examples
示例 1:分块流式计算文件 XXH3 哈希(可取消)
1// 分块读取输入流并计算 XXH3 哈希,在块边界响应停止且不按输入总大小占用内存
2inline auto hash_stream_to_hex(std::istream& stream, std::stop_token stop_token)
3 -> std::expected<std::string, std::string> {
4 // 每次只保留 1 MiB 输入,限制单个哈希任务的内存上限
5 std::vector<char> buffer(kReadBufferSize);
6
7 // 将 XXH3 状态绑定到官方释放函数,确保所有提前返回都能清理资源
8 auto state_result = create_state();
9 if (!state_result) {
10 return std::unexpected(state_result.error());
11 }
12 auto state = std::move(state_result.value());
13
14 bool has_data = false;
15
16 for (;;) {
17 // 停止后不再发起下一次同步读取,退出延迟最多受当前读取块影响
18 if (stop_token.stop_requested()) {
19 return std::unexpected("Hash calculation cancelled");
20 }
21
22 stream.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
23 const auto bytes_read = stream.gcount();
24
25 // 最后一轮即使遇到 EOF,也要先提交已经读到的剩余数据
26 if (bytes_read > 0) {
27 has_data = true;
28 auto update_result =
29 update_state(state.get(), buffer.data(), static_cast<std::size_t>(bytes_read));
30 if (!update_result) {
31 return std::unexpected(update_result.error());
32 }
33 }
34
35 // badbit 表示底层读取失败,不能把已读到的部分内容当成完整文件
36 if (stream.bad()) {
37 return std::unexpected("Input stream read failed");
38 }
39
40 // eofbit 表示输入已经正常读完,可以结束流式计算
41 if (stream.eof()) {
42 break;
43 }
44 }
45
46 // 保持现有业务语义:空文件不生成可用的媒体哈希
47 if (!has_data) {
48 return std::unexpected("Input stream is empty");
49 }
50
51 // digest 与原来一次性 XXH3_64bits 的结果保持一致
52 return digest_state(state.get());
53}Source: xxhash.hpp
示例 2:节流器的完整生命周期
1// 创建节流状态(无参数版)
2template <>
3inline auto create<void>(std::chrono::milliseconds interval)
4 -> std::unique_ptr<ThrottleState<void>> {
5 auto state = std::make_unique<ThrottleState<void>>();
6 state->interval = interval;
7 state->last_call_time = std::chrono::steady_clock::time_point{}; // epoch,允许立即首次调用
8 return state;
9}
10
11// 重置节流状态
12template <typename... Args>
13inline auto reset(ThrottleState<Args...>& state) -> void {
14 std::lock_guard lock(state.mutex);
15 state.has_pending = false;
16 state.last_call_time = std::chrono::steady_clock::time_point{};
17}Source: throttle.hpp
典型组合调用模式(基于以上真实 API 推导):auto st = utils::throttle::create<void>(16ms); 持有状态,在事件回调中 call(*st, fn),在事件序列结束时 flush(*st, fn) 补发末次,状态失效时 reset(*st)。
Configuration Options
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ThrottleState::interval | std::chrono::milliseconds | 16(≈60fps) | 两次真正执行之间的最小间隔;create() 时显式覆盖 |
kReadBufferSize | constexpr std::size_t | 1024 * 1024(1 MiB) | 哈希流式读取的固定块大小,即单任务内存上限 |
StreamRange::offset | std::uint64_t | 0 | 采样区间在流中的起始偏移 |
StreamRange::size | std::size_t | 0 | 采样区间长度(字节) |
无配置文件/环境变量参与——utils 层全部为编译期常量与运行时参数,刻意保持零外部依赖。
API Reference
utils::crypto
sha256_file(file_path) -> std::expected<std::string, std::string>
- 参数:
file_path(const std::filesystem::path&)目标文件路径。 - 返回:成功为小写十六进制 SHA-256 字符串;失败为错误描述字符串。
- 说明:实现在
crypto.cpp,本页未展开内部步骤。
utils::hash
create_state() -> std::expected<StatePtr, std::string>
- 返回:RAII 包装的 XXH3 流式状态;失败返回
"Failed to create XXH3 state"/"Failed to reset XXH3 state"。
update_state(state, data, size) -> std::expected<void, std::string>
- 参数:
state(XXH3_state_t*会话)、data(内存首地址)、size(字节数)。 - 行为:
size == 0时直接成功返回;XXH3_64bits_update非XXH_OK时返回"Failed to update XXH3 state"。
digest_state(state) -> std::string
- 返回:
std::format("{:016x}", ...)固定 16 字符十六进制摘要(64 位 XXH3)。
hash_stream_to_hex(stream, stop_token) -> std::expected<std::string, std::string>
- 参数:
stream(std::istream&)、stop_token(std::stop_token协作取消)。 - 返回/错误:
"Hash calculation cancelled"、"Input stream read failed"、"Input stream is empty",或 16 位十六进制指纹。
hash_stream_ranges_to_hex(stream, metadata, ranges, stop_token) -> std::expected<std::string, std::string>
- 参数:
metadata(std::span<const std::byte>固定宽度二进制元数据)、ranges(std::span<const StreamRange>采样区间列表)。 - 错误:含
"Input stream seek failed"与上述各错误。
utils::throttle
| 函数 | 签名 | 返回语义 |
|---|---|---|
create | create<Args...>(interval) -> std::unique_ptr<ThrottleState<Args...>> | 创建状态,last_call_time = epoch 允许立即首调 |
can_call | can_call(state) -> bool | 非线程安全的快速时间检查,不改状态 |
reset | reset(state) -> void | 清 pending、时间归 epoch |
call | call(state, func, args...) -> bool | true=已执行;false=被节流并缓存参数 |
flush | flush(state, func) -> bool | true=补发了 pending;false=无 pending |
utils::system
is_process_elevated() -> bool
- 修饰:
[[nodiscard]] noexcept。 - 返回:当前进程是否以管理员权限运行。
Professional Notes
失败模式与边界情况
| 场景 | 行为 | 依据 |
|---|---|---|
| 哈希中请求取消 | 每块读取前/区间内检查 stop_token,返回 "Hash calculation cancelled" | xxhash.hpp L66-L70 |
| 流底层读取失败(badbit) | 立即失败,不输出部分指纹 | xxhash.hpp L85-L88 |
| 最后一块遇到 EOF | 先 update 已读字节再退出循环 | xxhash.hpp L75-L83 |
| 空输入流 | unexpected("Input stream is empty"),不生成指纹 | xxhash.hpp L101-L104 |
seekg 失败(区间越界等) | unexpected("Input stream seek failed") | xxhash.hpp L136-L139 |
| XXH3 状态分配/重置失败 | expected 错误传播,RAII 保证无泄漏 | xxhash.hpp L19-L30 |
| XXH3 摘要兼容性 | 流式结果与一次性 XXH3_64bits 一致(源码注释明确声明) | xxhash.hpp L106 |
并发与一致性
ThrottleState内部有std::mutex:call/flush/reset均全程持锁,多线程共享同一状态对象是安全的。can_call刻意不加锁(源码注释:"这里读取不是线程安全的,仅用于快速检查"):这是一个性能优先的旁路检查,调用方不能依据它做需要一致性的决策,只能用于 UI 预判等宽容场景。- 持锁执行用户回调:
call在锁内std::forward<Func>(func)(args...)。若回调内部再进入同一ThrottleState的任何加锁函数将造成递归死锁——这是该 API 最需要防范的使用陷阱。 steady_clock防时钟跳变:节流时间基准不受系统时间修改影响。- 哈希模块本身无线程状态:取消协作性由调用方传入的
stop_token决定,模块可在任意线程池中复用。
性能与运维要点
- 内存上界确定:哈希任务峰值内存 ≈ 1 MiB(
kReadBufferSize)+ XXH3 状态,与输入规模无关,适合对超大媒体文件并发计算指纹。 - 区间采样换速度:
hash_stream_ranges_to_hex只读元数据+若干区间,用于快速去重/比对场景,代价是抗碰撞性弱于全文哈希(因此才强调固定宽度元数据防拼接歧义)。 - 默认 16ms 节流档位:与 60fps 渲染节奏对齐;重载为 UI 独立状态对象(每控件一个)而不是共享全局状态,避免相互饿死。
- 锁粒度:throttle 锁保护的是状态机本身而非回调执行副作用;长时间回调会拉长锁窗口,应保持回调轻量(入队/置脏标记),重活交给消费线程。
扩展点
- 新增哈希算法:仿照
StatePtr+create/update/digest三件套模式包装(如 BLAKE3),即可无缝接入现有std::expected错误通道与stop_token取消协议。 - 新增节流语义:
ThrottleState的pending_args/has_pending已构成最小状态机,可实现 debounce(延迟首执行)变体而不改动现有调用方。 - 新增系统查询:沿用
[[nodiscard]] ... noexcept的薄函数风格加入utils/system,保持无状态。
Tests
本次源码预算(6 次调用)内未检索到 src/utils 专属测试文件;如仓库存在针对 hash/throttle 的单测,属后续补充项,本页不做无依据的覆盖声明。
Related Links
- 哈希实现源码:xxhash.hpp
- 节流实现源码:throttle.hpp
- 加密接口:crypto.hpp / 实现:crypto.cpp
- 系统查询接口:system.hpp
- 图像辅助:image.hpp
- 兄弟主题(本仓库其他目录未在预算内展开):图形捕获(
utils/graphics)、显示几何(utils/display)、媒体编码(utils/media)、文件与 MIME(utils/file)、崩溃转储(utils/crash_dump)、日志(utils/logger)。