Repository Wiki
ChanIok/SpinningMomo

通用工具库(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::cryptoSHA-256 文件摘要
src/utils/hash/utils::hashXXH3 流式哈希与区间采样指纹
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/—对话框、崩溃转储、日志(兄弟页面)

关键架构约定(从已读源码可归纳):

  1. 错误处理统一为 std::expected<T, std::string>:所有可能失败的工具函数都返回 std::expected,错误类型为字符串描述(如 "Hash calculation cancelled"、"Input stream read failed"),调用方用 if (!result) / .error() 消费,不使用异常。
  2. 统一 vendor 标准库封装头:每个模块都 #include "vendor/std.hpp",将标准库依赖集中收敛到一处,便于切换编译器/C++ 标准配置。
  3. C++20/23 特性大量使用:std::stop_token(协作式取消)、std::span、std::format、[[nodiscard]]、CTAD 友好的模板特化(ThrottleState<void>)。
  4. RAII 资源绑定:C 资源用 std::unique_ptr + 删除器绑定(如 StatePtr = std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)>),保证提前返回路径也能释放。
  5. 尾置返回类型 + auto:全库统一 auto fn(...) -> Ret 风格。

Architecture

下面是本页覆盖的五个子域及其依赖关系(实线为"调用/包含"依赖,数据库形节点为外部依赖):

Loading diagram...

设计意图解读:

  • 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 流式状态,内存占用与文件总大小无关:

Loading diagram...

关键点(对应源码注释的设计意图):

  1. 停止延迟有界:取消检查发生在每块读取之前,因此退出延迟最多受"当前正在进行的这一次同步读取"影响,不会等到整个文件读完。
  2. EOF 前先提交已读数据:gcount() > 0 时即使已置 EOF 也要先 update,避免丢失最后一块。
  3. badbit/failbit 区分语义:bad() 表示底层读取失败;fail() 在非 EOF 情况下同样视为失败——两者都不能把已读到的部分内容当成完整文件。
  4. 空文件语义:!has_data 返回 unexpected("Input stream is empty"),保持"空文件不生成可用媒体哈希"的业务约定。

加密与哈希实现详解

utils::crypto::sha256_file

唯一公开入口,签名来自头文件:

cpp
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::crypto

Source: crypto.hpp

设计要点:入参是 std::filesystem::path(而非字符串),由调用方负责编码;返回小写十六进制字符串,与常见校验和工具输出一致;失败路径返回 std::expected<std::string, std::string> 错误信息。实现体在 src/utils/crypto/crypto.cpp(本页源码预算内未读取,故不展开其内部调用序列)。

utils::hash:XXH3 流式会话三件套

cpp
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

cpp
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),保证最终状态不丢失。

状态模型

cpp
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

cpp
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

cpp
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,保证最终一次状态被落地。

状态转移图

Loading diagram...

进程与系统(utils/system)

该模块暴露 Windows 系统级查询,本页源码预算内确认的唯一公开签名:

cpp
// 检测当前进程是否以管理员权限运行 [[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 哈希(可取消)

cpp
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:节流器的完整生命周期

cpp
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::intervalstd::chrono::milliseconds16(≈60fps)两次真正执行之间的最小间隔;create() 时显式覆盖
kReadBufferSizeconstexpr std::size_t1024 * 1024(1 MiB)哈希流式读取的固定块大小,即单任务内存上限
StreamRange::offsetstd::uint64_t0采样区间在流中的起始偏移
StreamRange::sizestd::size_t0采样区间长度(字节)

无配置文件/环境变量参与——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

函数签名返回语义
createcreate<Args...>(interval) -> std::unique_ptr<ThrottleState<Args...>>创建状态,last_call_time = epoch 允许立即首调
can_callcan_call(state) -> bool非线程安全的快速时间检查,不改状态
resetreset(state) -> void清 pending、时间归 epoch
callcall(state, func, args...) -> booltrue=已执行;false=被节流并缓存参数
flushflush(state, func) -> booltrue=补发了 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 的单测,属后续补充项,本页不做无依据的覆盖声明。

  • 哈希实现源码: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)。

Sources

(3 files)
src/utils/crypto
src/utils/hash
src/utils/throttle