Skip to content

feat(asio): 导出文件 I/O、四组 error 枚举、async_connect 与 make_address - #213

Merged
Sunrisepeak merged 1 commit into
mainfrom
feat/asio-file-io-and-error-surface
Aug 17, 2026
Merged

feat(asio): 导出文件 I/O、四组 error 枚举、async_connect 与 make_address#213
Sunrisepeak merged 1 commit into
mainfrom
feat/asio-file-io-and-error-surface

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

动机

chriskohlhoff.asio 的模块导出面在三处挡住了真实消费者。这些缺口是把一个 66k 行的 Windows 桌面工程(SpinningMomo)从 #include <asio.hpp> 迁到 import asio; 的过程中逐条撞出来的,不是猜的。

1. error 条件只导出了 operation_aborted

消费者能认出「取消」,认不出别的。区分「连接被拒」「超时」「DNS 查不到」在模块边界这一侧根本没有名字可比 —— 而这是任何网络重试逻辑要做的第一件事。

改为导出四个枚举类型 basic_errors / netdb_errors / addrinfo_errors / misc_errors,并用 using enum 带出枚举量。用 using enum 而不是逐个 using 是有意的:后者会变成一张 ~40 项的手工清单,上游加一个错误码它就悄悄过期。

2. 文件 I/O 完全没有

asio::stream_file / asio::random_access_file / asio::file_base(及其 basic_ 模板)现在在 ASIO_HAS_FILE 成立时导出。

守卫用 Asio 自己的能力宏,不是描述符里维护的平台清单 —— Windows 下后端是 IOCP,Linux 下需要 io_uring,macOS 下没有后端(asio/detail/config.hpp// Files. 一节)。这样它随上游演进,不需要这里跟着改。

⚠️ 一个容易踩的点,已写进描述符注释和测试:消费端不能用 #if defined(ASIO_HAS_FILE) 判断。宏不跨模块边界,而模块消费者不 include Asio 头文件,那个宏在它的 TU 里永远为假。就本包的配置而言判据是确定的 —— 未开启 io_uring,所以 ASIO_HAS_FILE ⇔ Windows,跨平台消费者按 _WIN32 分支即可。

3. asio::async_connectasio::ip::make_address 缺失

  • async_connect 是接受候选端点序列的自由函数。成员版 socket.async_connect 只接受单个端点,替代不了「把 resolver 的 results_type 交给它、逐个试」这个标准写法。
  • make_address 是从字符串解析地址的入口,而 address / address_v4 / address_v6 三个类型本来就已经导出了 —— 有类型没有构造入口。一并补上 make_address_v4 / make_address_v6 以保持对称。

测试

tests/examples/asio-module 新增两个成员测试、扩充一个:

  • errors.cpp(新):四个枚举类型 + 九个枚举量经 make_error_code 往返,断言 ec == asio::error::eof 这类真实写法成立(这只有在枚举量的名字跨过模块边界时才编得过);断言不同条件不互相塌缩;make_address / _v4 / _v6 的解析成功与失败两条路径。
  • file.cpp(新):stream_file 写入 + 读回(write_only|create|truncate 三标志组合)、random_access_file 按偏移读、basic_ 模板别名与基类关系的 static_assert。非 Windows 腿只打印「本配置无文件后端」并返回 0。
  • network.cpp:补自由函数 async_connect 的语义断言 —— 候选序列第一个是 loopback:1(不可达),必须跳过它连上第二个真实 acceptor。完全自足,不依赖 DNS。

验证

$ mcpp test -p asio-module     # linux / gcc@16.1.0
test result ok. 7 passed; 0 failed

lint 本地全过(loadfile / check_mirror_urls / check_package_name / check_platform_version_parity / mcpp xpkg parse)。

描述符头部的「未导出的组件」清单已同步:文件 I/O 从该清单移到新的「平台条件导出」一节,串口与 pipe 留在原处。

`import asio;` 的导出面此前少了三类消费者绕不过去的东西:

1. **error 条件只有 operation_aborted。** 消费者能认出取消,但认不出别的 ——
   区分「连接被拒」和「DNS 解析失败」在模块边界这侧没有名字可比。改为导出
   `basic_errors` / `netdb_errors` / `addrinfo_errors` / `misc_errors` 四个枚举
   类型,并用 `using enum` 带出枚举量,避免变成一张会随上游漂移的 ~40 项手工清单。

2. **文件 I/O 完全没有。** `asio::stream_file` / `random_access_file` /
   `file_base`(及 basic_ 模板)在 `ASIO_HAS_FILE` 成立时导出。用 Asio 自己的
   能力宏而不是在描述符里维护平台清单:Windows 下由 IOCP 提供,Linux 下需要
   io_uring,macOS 无后端。本包未开启 io_uring,所以就本包而言判据 ⇔ Windows。

   注意消费端**不能**用 `#if defined(ASIO_HAS_FILE)` 判断 —— 宏不跨模块边界,
   模块消费者不 include Asio 头文件,那个宏在它的 TU 里永远为假。描述符注释和
   tests/file.cpp 都写明了按 `_WIN32` 分支。

3. **`asio::async_connect` 与 `asio::ip::make_address` 缺失。** 前者是接受候选
   端点序列的自由函数(成员版 `socket.async_connect` 只接受单个端点,替代不了
   把 resolver 结果交给它的写法);后者是从字符串解析地址的标准入口,而
   address / address_v4 / address_v6 三个类型本来就已导出。

测试(tests/examples/asio-module):
- 新增 `errors.cpp`:四个枚举类型 + 九个枚举量经 make_error_code 往返,断言
  `ec == asio::error::eof` 这类真实写法成立,并断言不同条件不互相塌缩;
  make_address / make_address_v4 / make_address_v6 的解析成功与失败路径。
- 新增 `file.cpp`:stream_file 写入 + 读回(三个 file_base 标志组合)、
  random_access_file 按偏移读;非 Windows 腿只记录「本配置无文件后端」。
- `network.cpp` 补自由函数 async_connect 的语义断言:候选序列里第一个不可达
  (loopback:1),必须跳过它连上第二个。不依赖 DNS,保持自足。

本地 linux/gcc16:`mcpp test -p asio-module` 7 passed。
@Sunrisepeak
Sunrisepeak merged commit 4db48ae into main Aug 17, 2026
11 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/asio-file-io-and-error-surface branch August 17, 2026 18:54
Sunrisepeak added a commit to Sunrisepeak/SpinningMomo that referenced this pull request Aug 17, 2026
上一轮 full-build 走到了业务代码编译阶段(依赖、CRT、链接全部就位),只在
一处失败,而且是已知的那处:

  root_availability.cpp(238): fatal error C1116: unrecoverable error importing
  module 'utils.network.network'.  Specialization of
  'asio::detail::service_registry::use_service' with arguments 'asio::config_service'

根因是 asio 被 N 个模块各自的 GMF 分别 #include:附着于全局模块的特化在每个
导入方被重新实例化,MSVC 调和不了。`import asio;` 让它们只在 asio 模块内实例化
一次 —— 这就是「asio 必须用 module 方式」的技术含义。

## 转换范围由约束推出,不是挑的

模块单元的 purview 里不能 #include,而头文件根本不能 import。所以一旦 asio 变成
模块,直接 include 它的**头文件**必须跟着变模块,并沿 include 图向上传播,直到
遇到 .cpp —— .cpp 可以 import 而不必自己变成模块,传播到此为止。

`mcpp-modularize.py --closure vendor/asio.hpp` 把这条规则固化成脚本:18 个头文件
(其中 pch 单独处理),45 个 .cpp/.cppm 改 include 为 import,共 63 / 497 个文件。

## 三件配套

1. **删除 src/pch.hpp 与 xmake 的 set_pcxxheader。** PCH 是文本快照,跟具名模块
   对同一份 SDK 头会给出不同的实体归属;mcpp 本来就没有 PCH。留着它,一个 TU 会
   同时文本包含 asio 和 import asio。

2. **17 个旧模块补 sm. 前缀。** 它们建成时没加,因为 mcpp 禁的是 `util` 而不是
   `utils`,侥幸合法。但一棵树里一半带前缀一半不带,规则就变成了「看上一批怎么
   做的」。`--rename-prefix utils` 一次改齐。

3. **两个新的转换 pass。** `--normalize` 把落进 GMF 的 import 移进 purview
   (rewrite-consumers 原地替换 include,而在已转换的模块里那行 include 正好在
   GMF —— 替换是对的,留在原地不对),并删掉因此空掉的 `module;`。

## asio 描述符

导出面缺 11 个本项目实际调用的名字。修订已合入上游
mcpplibs/mcpp-index#213(Windows/macOS/Linux/llvm 四条腿全绿)。已发布的索引
快照还没跟上,所以 mcpp/pkgs/a/sm.asio.lua 是一份**临时覆盖**,内容逐字复制自
合入后的官方描述符。`scripts/check-asio-override.sh` 在 Linux 上几秒钟回答
「可以删了吗」,不用花一轮 Windows CI。

新增 probes/p6-asio-module:按 SpinningMomo 实际调用面断言 module 形态的 asio
(make_address / 六个 error 条件 / 自由函数 async_connect / stream_file +
random_access_file)。文件 I/O 段按 _WIN32 门控 —— 宏不跨模块边界,消费者 TU 里
ASIO_HAS_FILE 恒假;这样这个探针在本地 Linux 上也能跑,已通过。

## 架构守卫跟上现实

check-cpp-architecture.py 还在按 Headers/PCH 时代检查(要求每个文件都
`#include "vendor/std.hpp"`,且完全不看 .cppm)。改为:
- 扫描 .cppm;
- 标准库一个单元一扇门:模块单元 `import std;`,普通 TU `vendor/std.hpp`;
- utils::hash 的来源两种拼法都认;
- 新增「模块接口只放声明」检查 —— 模板 / inline / constexpr / 类内成员不算。
Sunrisepeak added a commit to Sunrisepeak/SpinningMomo that referenced this pull request Aug 17, 2026
## 1. 生态闭环(P7)

`mcpp.toml` 不再有 `[indices]`,`mcpp/` 目录整个删除。依赖来源:

  chriskohlhoff.asio   mcpplibs/mcpp-index#213(导出面**修订**,不是另起一个包)
  compat.xxhash        #214
  compat.libwebp       #214
  compat.reflectcpp    #214
  compat.sqlitecpp     #215  → compat.sqlite3
  compat.usockets      #215  → compat.libuv
  compat.uwebsockets   #215  → compat.usockets
  compat.wil           #216(Windows-only,[target.'cfg(windows)'] 门控)

本地已验证整套依赖能从**已发布**的索引解析并构建(linux/gcc16,10.3s)。

**sm.asio 覆盖描述符可以删**,因为它的两个理由都没了:导出面由 #213 补齐;而
`_WIN32_WINNT` 那个 pin —— p8 第三轮把它拿掉、直接依赖官方包,**绿**。做功的一直是
「include 在 import 之前」那条规则。

**WebView2 不在里面,这是判断不是遗漏。** 它是 NuGet **二进制**分发(头文件 + 预编译
loader `.lib`),索引的 A–H 形态分类里没有这一档,引入新形态是比这组 PR 大得多的讨论。
它继续走 `scripts/mcpp-prebuild.sh` 的解包,经 include_dirs/ldflags 到达构建。闭环的
判据是「工程 mcpp.toml 不再声明 `[indices]`」,不是「所有第三方物料都进索引」——
这条写在 mcpp.toml 的注释里,免得下一个人以为是漏了。

连带清理:`mcpp-link-index.sh`(path 索引在 Windows 上要 junction 的绕行)、
`check-asio-override.sh`(问题已有答案)、CI 两处索引注册步骤与为索引物化设的 build
重试、parity 检查的候选顺序。

**p5-index → p5-official-index**:同一个探针反过来问 —— 从「项目自有索引端到端」变成
「SpinningMomo 的整套依赖,从已发布索引解析」,而且是在一个从没见过本仓库描述符的
runner 上。八个包各按真实用法跑(asio 走 module、其余走头文件),六分钟给出生态闭环的
答案而不是四十分钟。

## 2. 补齐七处 vendor 头,并加一条守卫

  features/screenshot/state.cppm:43: error: use of undeclared identifier 'winrt'

模块化拿走了一个一直在悄悄工作的通道:转换前这个文件通过
`#include "utils/graphics/capture.hpp"` 顺带拿到了 WinRT 投影头;`capture` 现在是模块,
而模块的 GMF **不是名字的传递通道** —— 里面的实体附着于全局模块,但那个 TU 里没有
`winrt` 这个名字的声明。项目本来就要求「每个 TU 自包含」,只是以前有传递包含兜底。

  winrt::            screenshot/state.cppm, recording/recording.cpp,
                     overlay/capture.cpp, preview/capture.cpp
  Microsoft::WRL::   core/webview/{static,host}.cpp
  wil::              ui/notification_window/painter.cpp

七处同一个错误,每处单独暴露要一轮 40 分钟的构建,所以加规则而不是逐个修:守卫按
**目录**匹配,`vendor/windows/winrt/` 下新增门面当天就被覆盖。已反向验证。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant