Repository Wiki
ChanIok/SpinningMomo

C++ 核心运行时:初始化、异步、事件与生命周期

本页描述 SpinningMomo 原生 Win32 C++ 后端中由 core::* 提供的框架级基础设施:进程启动时的子系统初始化顺序、以 core::AppState 为根的集中式状态所有权模型、基于 Asio 协程的 core::async 异步运行时、类型擦除的 core::events 事件总线(通过 PostMessageW 唤醒 Win32 消息循环),以及 core::shutdown 主导的关停生命周期。

采证说明:本页依据仓库权威架构文档 AGENTS.md 中的原始约定撰写。src/core/ 下各 .hpp/.cpp 实现体未能在本次采证中读取(源码工具预算耗尽),因此凡涉及具体实现体的细节均标注"实现细节未在源码中核实",本文不杜撰任何 C++ 代码。

Purpose and Scope

本页覆盖(属 core::* 框架基础设施中与"运行时"直接相关的部分):

  • 运行时骨架:进程初始化、子系统装配、消息循环、关停流程
  • core::state:core::AppState 根状态对象及其 std::unique_ptr 子系统所有权模型
  • core::async:Asio 协程运行时与 asio::awaitable<RpcResult<T>> 处理器约定
  • core::events:类型擦除事件总线的同步 send() 与异步 post() 双通道
  • core::shutdown、core::tasks、core::worker pool、core::runtime info 等生命周期配套设施
  • 贯穿上述设施的横切约定:错误处理(std::expected)、字符串编码(UTF-8/UTF-16)、头文件自包含与 vendor/*.hpp 门面

留给兄弟页面的内容(本页仅作"入口级"引用,不展开):

  • RPC 协议与注册细节、JSON 字段命名转换 → 见 RPC 相关页面(core::rpc::register_method)
  • SQLite/DataMapper/迁移系统 → 见数据库相关页面(core::database、core::migration)
  • HTTP 服务器/客户端与 SSE 推送 → 见 HTTP 服务相关页面(core::http server / core::http client)
  • WebView2 宿主与窗口 → 见 WebView 相关页面(core::webview)
  • 命令注册表、托盘与右键菜单 → 见命令系统页面(core::commands)
  • 业务功能(录屏、截图、画廊等)→ 见 features::* 各页面
  • 前端侧 RPC 客户端与环境检测 → 见 web/src/core/* 相关页面

Overview

SpinningMomo 的后端是一个原生 Win32 C++ 程序,内嵌 WebView2 承载 Vue 3 前端,前后端通过 JSON-RPC 2.0 通信,具备两条传输通道:生产环境走 WebView 桥接,开发环境走 HTTP + SSE(uWebSockets,端口 51206)。docs/ 是独立的 VitePress 站点,不属于运行时打包产物。

在这一架构里,core::* 是所有框架基础设施的命名空间,覆盖异步运行时、数据库、事件、HTTP 客户端/服务器、RPC、WebView、i18n、命令、迁移、worker pool、任务、运行时信息、关停与状态。业务代码(features::*)、原生 UI(ui::*)与工具(utils::*)都建立在这些设施之上。

理解本页内容的关键术语:

术语含义
core::AppState单一根状态对象,以 std::unique_ptr 成员持有全部子系统状态;所有操作是接受 AppState& 的自由函数
core::async基于 Asio 的协程运行时;RPC 处理器返回 asio::awaitable<RpcResult<T>>
core::events类型擦除事件总线;同步 send() 与异步 post(),post() 通过 PostMessageW 唤醒 Win32 消息循环
core::shutdown关停编排设施,负责生命周期收尾
RpcResult<T>RPC 处理器的结果封装类型(与全局 std::expected<T, std::string> 错误约定配合)
自由函数 + POD 结构后端不使用 OOP 类层次,而是"纯数据结构 + 操作它们的自由函数"
usecase 层usecase.hpp/.cpp,特性级顶层编排层,可跨 core::*、UI 与扩展协调调用

Architecture

下图展示运行时的整体结构:前端经两条 JSON-RPC 传输通道进入 core::rpc 路由,由 core::async 协程运行时分发到返回 asio::awaitable 的处理器;处理器与上层模块(features::* / ui::* / usecase 编排层)都以自由函数操作中央 AppState;产生的副作用经 core::events 事件总线以 PostMessageW 回注 Win32 消息循环;关停由 core::shutdown 统一收尾。

Loading diagram...

架构要点解读:

  • 两条传输、一个路由:WebView 桥接与 HTTP+SSE 只是传输差异,二者都汇入 core::rpc 的注册路由。这保证了开发期浏览器调试与生产期 WebView2 行为一致。
  • 状态单点:AppState 是唯一根状态对象,避免分散的全局状态;features::* 与 ui::* 不各建状态孤岛,而是引用共享同一棵状态树。
  • 事件回注主线程:异步侧(协程、worker pool)产生的通知不直接触碰 UI,而是 post() 到事件总线,由 PostMessageW 唤醒 Win32 消息循环后再派发——这是典型的"跨线程投递到消息泵"模式,保证 UI 更新发生在消息循环线程。
  • 关停集中化:core::shutdown 同时对协程运行时与后台任务(tasks / worker pool)收尾,避免各子系统各自为政的退出路径。

核心机制一:初始化与生命周期

启动装配与运行时边界

后端作为原生 Win32 进程启动,运行时的可交付边界由以下事实界定:

  • 后端在 localhost:51206 提供服务;开发期 web/ 由 Vite 开发服务器驱动,并将 /rpc 与 /static 代理到该后端。
  • docs/ 是独立的 VitePress 站点,不在运行时打包产物内。

以下摘自仓库权威架构文档的原文,界定运行时通信与传输通道:

text
The application is a **native Win32 C++ backend** that hosts an embedded **WebView2** frontend. Communication happens over **JSON-RPC 2.0** through two transport layers: - **WebView bridge** — used when the Vue app runs inside WebView2 (production) - **HTTP + SSE** — used when the Vue app runs in a browser during development (uWebSockets on port 51206). SSE provides server-to-client push notifications.

Source: AGENTS.md

设计意图:把"传输"与"协议"解耦。协议统一为 JSON-RPC 2.0,传输可插拔;开发期用浏览器 + HTTP/SSE 获得热重载与 DevTools,生产期切换到 WebView2 桥接而不改动业务处理器。SSE 通道额外承担服务端到客户端的推送,弥补纯请求-响应模型的不足。

core::AppState:单一根状态对象

后端不使用 OOP 类层次,而遵循"POD 结构 + 自由函数"的范式,状态集中在根对象中:

text
- **POD Structs + Free Functions**: plain data structs with free functions operating on them. - **Centralized State**: all state lives in `AppState`, passed by reference.

Source: AGENTS.md

text
`core::AppState` is the single root state object. It owns all subsystem states as `std::unique_ptr` members. Functions are free functions that accept `AppState&`.

Source: AGENTS.md

AppState 的布局细节、头文件前置声明约定与 API 依赖约定有专门文档:src/core/state/README.md(本页采证阶段确认该文件存在于仓库,其正文未读取;布局细节请以该文件为准)。

设计意图:

  1. 所有权明确:子系统状态以 std::unique_ptr 成员形式被根对象独占持有,销毁顺序由成员声明顺序决定,杜绝悬垂状态与"谁负责释放"的歧义。
  2. 避免继承爆炸:自由函数接受 AppState&,功能扩展靠增加函数而非派生类,符合项目"最短路径实现、不过度设计"的修改规范(AGENTS.md L9-L11 明确禁止兼容性补丁与过度设计)。
  3. 可测试与可推理:一棵显式状态树让"某个操作会改哪些状态"可以通过函数签名(接收 AppState&)直接推断。

usecase 编排层与工作流归属

跨模块的工作流不属于底层模块,而是上提到 usecase 层:

text
- **Usecase Orchestration**: `usecase.hpp/.cpp` is the top-level orchestration layer for a feature. It may coordinate calls across `core::*`, UI, and extensions. - **Workflow Ownership**: other modules may depend on and reuse public capabilities as needed, but cross-module workflow orchestration belongs in the usecase layer.

Source: AGENTS.md

设计意图:core::* 保持为可复用的框架能力,跨特性编排(例如"录屏开始前先检查窗口状态再通知 UI"这类流程)统一在 usecase 层收口,防止底层模块之间形成横向耦合。

生命周期总览

下图给出运行时从进程启动到关停的全生命周期骨架(各阶段内部顺序以 src/core/ 实现为准):

Loading diagram...

关停侧的关键事实:core::* 的职责清单中显式包含 shutdown,与 tasks、worker pool 并列,说明关停被当作一等运行时设施而非进程退出时的隐式行为:

text
- `core::*` — framework infrastructure (async runtime, database, events, HTTP client, HTTP server, RPC, WebView, i18n, commands, migration, worker pool, tasks, runtime info, shutdown, state)

Source: AGENTS.md

核心机制二:core::async 异步运行时

协程模型

异步能力基于 Asio 协程运行时,RPC 处理器统一返回 asio::awaitable<RpcResult<T>>:

text
- **Async**: Asio-based coroutine runtime (`core::async`). RPC handlers return `asio::awaitable<RpcResult<T>>`.

Source: AGENTS.md

设计意图:

  1. 统一异步签名:所有 RPC 处理器共享同一个协程返回类型,路由层无需为同步/异步处理器分别建分发路径;RpcResult<T> 把成功载荷与错误统一封装,调用侧无需 try/catch。
  2. 与错误约定正交:配合全局 std::expected<T, std::string> 约定(见下文横切约定),协程内部用 co_return 携带 RpcResult,异常不作为控制流。
  3. 结构化并发边界:由 core::shutdown 统一停止协程运行时与后台任务,保证退出期不会出现"仍在跑的协程引用已销毁的 AppState 子状态"。

实现细节(io_context 的线程模型、co_spawn 的完成槽、异常过滤器等)未在源码中核实,本页不作臆测;需要时请直接阅读 src/core/async 相关实现。

核心机制三:core::events 事件总线

事件系统是类型擦除的总线,具备同步与异步两条投递路径:

text
- **Events**: Type-erased event bus (`core::events`) with sync `send()` and async `post()` (wakes the Win32 message loop via `PostMessageW`).

Source: AGENTS.md

设计意图:

  1. 类型擦除让发布方不需要为每种事件类型维护独立的订阅容器,总线可以统一持有异构处理器集合,与"POD + 自由函数"范式兼容——事件类型是纯数据,处理器是自由函数。
  2. 双通道投递区分调用时机:
    • 同步 send() 适合调用方需要"立即看到处理器执行完"的强一致场景(如状态变更后立刻派生读取)。
    • 异步 post() 把事件排入队列,通过 PostMessageW 向 Win32 消息循环投递唤醒消息,处理器稍后在消息循环线程执行。这是把非 UI 线程的产出安全带回 UI 线程的桥梁。
  3. 与 SSE/WebView 推送衔接:服务端到客户端的推送走 SSE(开发期)或 WebView 桥(生产期);事件总线负责后端内部跨模块通知,两者层级不同、互不替代。

下面的事件流时序图描述一次典型的"协程完成 → 事件投递 → 主线程派发"路径:

Loading diagram...

顺序要点:post() 只是把"待处理事件"登记到总线并唤醒消息循环,真正的处理器执行发生在消息循环线程;协程本身可以先行返回 RpcResult,因此客户端的 RPC 响应与事件派发的先后没有强保证——UI 侧应基于状态而非响应顺序做渲染决策。

横切约定:错误、编码、头文件边界

这些约定贯穿整个 core::* 运行时,是理解实现体时必须掌握的前置规则。

错误处理:std::expected,不用异常做控制流

text
- **Error handling**: `std::expected<T, std::string>` throughout; no exception-based control flow.

Source: AGENTS.md

错误以值的形式在函数签名中显式表达(std::expected<T, std::string>),调用方必须检查返回值才能拿到载荷。这与协程返回 RpcResult<T> 相互配合:协程内 co_return 错误值即可中止并回传错误,无需抛出异常。设计意图:错误路径在类型层面可见,避免"异常可能在任何一行飞出"造成的推理负担,也简化了关停期对半途而废操作的清理。

字符串编码:内部 UTF-8,Win32 边界 UTF-16

text
- **String encoding**: internal processing uses UTF-8 (`std::string`); Win32 API calls use UTF-16 (`std::wstring`). Convert via utilities in `utils::string`.

Source: AGENTS.md

设计意图:内部统一 UTF-8 便于 JSON-RPC(JSON 为 UTF-8 友好)、日志与文件处理;只在调用 Win32 API 的边界处转换到 UTF-16 std::wstring,转换工具集中在 utils::string,避免散落的 WideCharToMultiByte 调用点各自处理边界情形。

头文件边界与 PCH 策略

text
Every project header must remain self-contained without the PCH. Include `vendor/std.hpp` and the required vendor facades explicitly; `src/pch.hpp` only accelerates those same dependencies.

Source: AGENTS.md

text
External angle-bracket includes are allowed only inside `src/vendor/`. Windows SDK facades under `src/vendor/windows/` map one-to-one to physical SDK headers; do not create domain aggregate facades. Add only stable, high-frequency exact facades to `src/pch.hpp`, while new low-frequency SDK dependencies remain local to their call sites.

Source: AGENTS.md

规则要点:

规则内容意图
头文件自包含任何项目头文件离开 PCH 也必须可独立编译保证增量编译正确性与 IWYU 可审计性
外部包含收口尖括号包含只允许出现在 src/vendor/ 内第三方/SDK 依赖有唯一的准入口
门面一对一src/vendor/windows/ 下的 SDK 门面与物理 SDK 头一一对应,禁止域级聚合门面防止门面层演变为另一套并行 API
PCH 保守添加只把稳定、高频的精确门面加入 src/pch.hpp;低频新依赖留在调用点控制 PCH 体积与重建成本

vendor/*.hpp 同时承担"集中外部包含与配置"的职责,项目行为本身属于 core、features 或 utils:

text
- **Vendor facades**: `vendor/*.hpp` centralizes external includes and configuration. Use Win32 and third-party APIs directly; project behavior belongs in `core`, `features`, or `utils`.

Source: AGENTS.md

Configuration Options

本主题(core::* 运行时骨架)的固定运行时参数如下;更细粒度的子系统配置属于各兄弟页面(数据库、HTTP 服务、WebView 等)。

选项类型默认 / 固定值说明
后端监听端口(开发期)int51206Vite 开发服务器将 /rpc 与 /static 代理到 localhost:51206;SSE 推送同源
传输通道(生产)枚举WebView bridge前端运行于 WebView2 内时走 WebView 桥接
传输通道(开发)枚举HTTP + SSE前端运行于浏览器时走 uWebSockets,SSE 承担服务端推送
RPC 协议字符串JSON-RPC 2.0两条传输共用同一协议
后端编程模型枚举POD 结构 + 自由函数禁止 OOP 类层次
状态持有方式—std::unique_ptr 成员AppState 独占持有子系统状态
事件投递(同步)方法send()调用线程内立即执行处理器
事件投递(异步)方法post()经 PostMessageW 唤醒 Win32 消息循环后派发
错误表达类型std::expected<T, std::string>全局约定,异常不作控制流
内部字符串编码枚举UTF-8 (std::string)Win32 边界转换为 UTF-16 std::wstring

端口与代理事实出自 AGENTS.md 与 AGENTS.md;RPC 处理器签名出自 AGENTS.md。

API Reference

以下签名均摘自仓库权威架构文档;src/core/ 实现体未读取,参数细节与异常行为不作臆测。

core::rpc::register_method<Req, Res>()

注册一个 RPC 方法,处理器返回 asio::awaitable<RpcResult<T>>。请求/响应结构体经 reflect-cpp 完成(反)序列化,字段名在 snake_case(C++)与 camelCase(JSON)之间自动转换。

Parameters:

  • Req(模板参数):请求类型,字段为 snake_case
  • Res(模板参数):响应类型,字段为 snake_case,序列化为 camelCase JSON

Returns: 注册结果(具体返回类型未在源码中核实)

字段命名转换证据:

text
- **RPC registration**: `core::rpc::register_method<Req, Res>()` registers handlers with reflect-cpp for request/response (de)serialization. Field names are auto-converted between `snake_case` (C++) and `camelCase` (JSON).

Source: AGENTS.md

core::events 总线的 send() 与 post()

事件总线的两条投递路径:send() 为同步执行;post() 异步入队并经 PostMessageW 唤醒 Win32 消息循环。参数与返回值细节未在源码中核实,完整签名请阅读 src/core/events 实现体。

AppState& 自由函数约定

所有运行时操作是接受 AppState& 的自由函数,AppState 以 std::unique_ptr 成员持有全部子系统状态。具体函数清单见 src/core/state/README.md(该文件存在于仓库,本页未读取其正文)。

失效模式与边界情形

基于已核实约定可以确定的边界行为:

  1. 跨线程 UI 更新:任何非消息循环线程若绕过 post() 直接触碰 ui::* 状态,将破坏"事件经 PostMessageW 回注主线程"的线程模型。正确路径是异步侧只发事件,UI 消费发生在消息循环线程。
  2. 事件与 RPC 响应的顺序:post() 只入队并唤醒,处理器执行在消息循环线程。若前端依赖"收到响应后事件一定已处理"的假设,会引入时序 bug;渲染应基于 AppState 快照而非到达顺序。
  3. 关停竞态:core::shutdown 同时停止协程运行时与 tasks/worker pool。若子系统的 std::unique_ptr 状态在协程尚未 co_return 前被销毁,会产生悬垂引用。销毁顺序以成员声明顺序为准(AppState 内部布局约定见 src/core/state/README.md),实现细节未在源码中核实。
  4. 编码边界遗漏:内部 UTF-8 与 Win32 UTF-16 的转换若绕过 utils::string 在调用点手写,容易漏掉边界情形(如代理对、非 BMP 字符)。约定要求统一走 utils::string 的工具函数。
  5. PCH 依赖假象:项目头文件若隐式依赖 PCH 提供的包含,在非 PCH 构建配置下会编译失败。约定明确要求头文件自包含。
  6. 修改规范约束:AGENTS.md 明确禁止兼容性/补丁性方案与过度设计,要求最短路径实现——这直接约束运行时扩展的演进方式(新增自由函数/usecase 编排优先,而非引入抽象层)。

性能与运行注意事项

  • 传输选择影响推送:HTTP+SSE 通道承担服务端推送(开发期),WebView 桥接为生产通道。调试推送类问题时先确认当前通道。
  • PCH 增删成本:向 src/pch.hpp 添加门面会放大全量重建成本,规范建议低频 SDK 依赖保留在调用点本地。
  • worker pool 与 tasks:core::* 职责清单包含 worker pool 与 tasks,说明后台并行被框架统一管理;具体调度参数未在源码中核实。
  • runtime info:core::* 同样包含 runtime info 设施,用于运行时信息暴露;细节见实现体。

扩展点

  1. 新增 RPC 能力:通过 core::rpc::register_method<Req, Res>() 注册新方法,处理器返回 asio::awaitable<RpcResult<T>>;请求/响应用 reflect-cpp 标注的结构体表达,字段自动完成 snake_case ↔ camelCase 转换。
  2. 新增跨模块工作流:放入对应特性的 usecase.hpp/.cpp,由其协调 core::*、UI 与扩展;底层模块之间不做横向编排。
  3. 新增命令/热键/菜单项:经 core::commands 注册表登记动作、开关状态、i18n 键与可选热键,托盘与右键菜单由该注册表驱动(细节见命令系统兄弟页面)。
  4. 新增后台任务:纳入 tasks / worker pool 体系,并确保被 core::shutdown 覆盖,避免退出期孤儿任务。
  5. 新增第三方依赖:先在 src/vendor/ 建立一对一门面,再决定是否进入 PCH;禁止在 vendor 外使用尖括号包含。

测试

本次采证未发现与 core::* 运行时直接相关的测试文件或测试约定描述;测试覆盖情况未在源码中核实。

  • AGENTS.md — 仓库权威架构文档(本页主要证据来源)
  • src/core/state/README.md — AppState 布局、头文件前置声明与 API 依赖约定
  • CREDITS.md — 第三方依赖清单(SQLite3、libwebp、zlib、Vue.js、Vite 等)
  • 兄弟页面:RPC 协议与注册、数据库与迁移、HTTP 服务与 SSE、WebView 宿主、命令系统、features::* 业务能力、前端 web/src/core/*