- 日期:2026-07-20
- 分支:
feat/mcpp-provider - 配套调研:
2026-07-19-mcpp-replace-xmake-research.md(xmake → mcpp 的可行性论证与 rustlings/cargo 横向对照) - d2x 侧协议规范:d2x 仓库
.agents/docs/2026-07-20-d2x-architecture-reference.md - 本文定位:当前实现的参考手册——目录约定、生成物、判定机制、维护须知
⚠️ 2026-07-23 起本文的 §4(清单生成)与 §5(判定机制)已被「练习即测试」迁移取代,现状见2026-07-23-exercises-as-tests-reference.md。
xmake 已完全退役。构建链路是:
d2x(通用框架,零内置后端)
└─ Provider Protocol (NDJSON) ─→ d2x/buildtools/mcpp/ ← 本仓库自己的 Provider(C++26)
├─ 目录约定发现练习
├─ 生成 mcpp workspace 清单
├─ 调 mcpp build/run
└─ 读侧信道判定通过
↓
mcpp(未做任何改动)
mcpp.toml [workspace] members = ["d2x/buildtools/mcpp", "dslings/harness"]
d2x/buildtools/mcpp/ Provider,standard = "c++26"
dslings/harness/ 练习脚手架库,standard = "c++23"
.d2x/build/mcpp.toml 独立 workspace 根
.d2x/build/<std>/mcpp.toml 该标准下全部 target —— 供 clangd
.d2x/build/_current/mcpp.toml 只含当前一题 —— 供 checker
.d2x/build/_current/result.ndjson 判定用侧信道
.d2x/state.json 学习者进度(d2x 写)
.d2x.json:
{ "buildtools": "mcpp run -q -p d2x/buildtools/mcpp --" }d2x 拼接后得到 mcpp run -q -p d2x/buildtools/mcpp -- check <id>。从仓库根执行、无需 cd、首次自动构建 Provider,暖开销约 26ms。
d2x 从不 chdir(它在静态初始化时捕获 CWD),所以免 cd 是硬要求。注意 mcpp 的 -p 匹配的是目录 basename 或完整相对路径,不是包名。
mcpp run 会把 LD_LIBRARY_PATH 指向它私有的 glibc 并注入子进程。Provider 再去 spawn 嵌套的 mcpp 时被迫加载错配的 glibc,在动态链接器里段错误(冷启动稳定复现,输出只剩 <pid>:\t__vdso_time 这样的 trace 残片)。
runner.cppm 在每次 spawn 前 unsetenv("LD_LIBRARY_PATH") 绕过;mcpp 会为它自己的子进程重新设置正确的值。
d2x 侧对同一问题早有相同处理(platform.cppm),仓库里还留着 workaround_ld_library_path_issue 分支——这是 mcpp 的既有问题,值得单独向上游报。
没有声明文件。 放一个 .cpp 进去就是全部工作。
rustlings 最贵的一课是 PR #1355:Rust edition 同时写在 rustc 参数和 rust-project.json 里,两边漂移酿成 bug。任何独立声明文件都是第二套真相源。 这里的真相只有两处,且都无法漂移:目录结构,和练习文件自己的头部注释。
dslings/hello-mcpp.cpp 入门练习(chapter = intro)
dslings/<std>/NN-topic-K.cpp zh 练习
dslings/en/<std>/NN-topic-K.cpp en 练习(lang=en 时启用,与 zh 互斥)
solutions/<std>/NN-topic-K.cpp 参考答案(zh/en 共用同一份)
| 文件 | id | chapter | order |
|---|---|---|---|
dslings/cpp11/00-auto-and-decltype-0.cpp |
cpp11-00-auto-and-decltype-0 |
cpp11/00-auto-and-decltype |
1100000 |
dslings/cpp11/03-trailing-return-type.cpp |
cpp11-03-trailing-return-type |
cpp11/03-trailing-return-type |
1100300 |
dslings/hello-mcpp.cpp |
hello-mcpp |
intro |
-100000 |
order = std_rank × 100000 + 章节号 × 100 + 练习序号。尾部 -<数字> 是序号,没有就当 0(仓库里 03/04 两章确实缺 -0 后缀,规则把它们归一化)。
写在练习文件自己的头部注释里(只扫前 40 行):
// d2x:cxxflags: -O0 -fno-elide-constructors仓库现存用例:04-rvalue-references(观察移动)、07-constexpr-0(-Wpedantic -Werror 让 VLA 成为错误)。
id 只允许 [A-Za-z0-9._-],违反者被拒绝并打印到 stderr,不进入枚举。
因为 id 有两个危险去向:d2x 拼进 shell 命令、我们写进生成的 TOML([targets.<id>])。在源头挡住比在两个下游各自转义更可靠——这类文件名本就是笔误或恶意 PR,与其想办法安全传递,不如让作者改名。
硬约束:dslings 的练习默认就编译不过(49 个带 D2X_YOUR_ANSWER),而 mcpp 的 build 和 run 都会先全量构建整个包——一个坏兄弟拖垮全部且零产物。实测:
member 含全部 3 题(后两题未做完)→ mcpp build -p → exit=1 ← 当前这题被拖挂
只生成当前这一题 → 0.071s,exit=0
member 粒度(标准/特性/练习)本身不解决逐题隔离。 所以分两个:
| 目录 | 内容 | 用途 |
|---|---|---|
.d2x/build/<std>/ |
该标准下全部 target | clangd —— 即使多数练习编译不过,mcpp 仍生成完整 compile_commands.json,IDE 对每题都有正确编译参数 |
.d2x/build/_current/ |
只含要验证的那一题 | checker |
实测动态改写清单代价极低:切题 0.118s、切回 0.018s,fingerprint 目录始终只有 1 个(改写 target 集合不会让缓存爆炸)。
main用../../../逃逸出包根,练习源文件原地不动- 脚手架走
[dependencies] path依赖,而非把仓库根塞进 include 搜索路径(后者会让练习能#include仓库里任何文件) -fmacro-prefix-map=<repo_root>/=让__FILE__相对仓库根,学习者看到的报错不再顶着一长串/home/...- 所有清单内容比对后才落盘,不推进 mtime,保住 mcpp 的快速路径
check会先write_full再write_current——全新仓库上学习者直接跑d2x checker时根清单还不存在,否则 mcpp 报退出码 2
判定原先 100% 是带内信号:断言失败并不改变退出码(实测 exit=0),所以只能扫 stdout 找 ❌。这会误判——一个断言全过的正确解答,只要在说明文字里打了个 ❌ 就被判失败(已复现)。反过来,输出截断或 Windows 控制台代码页不对,判定同样失效。
现在 stdout 回归「给人看」,判定走侧信道:
逐条追加而非退出时统一写 —— 练习段错误时,崩溃之前的断言结果照样保留。
D2X_RESULT_FILE 环境变量未设置时 harness 只打印不写文件:学习者直接跑二进制零摩擦。
| 侧信道内容 | Verdict |
|---|---|
有 ok:false |
fail + 每条失败转一个 Diagnostic |
| 无失败但退出码非 0 | fail |
无失败、有 wait |
blocked |
| 文件不存在(练习没用 harness) | 退回「编译通过 + 退出 0 = pass」 |
最后一条让 harness 自动变成可选的,不需要额外机制:纯观察型练习(现有 18 个只用 D2X_WAIT)可以写成零依赖的纯 C++ 文件,学习者能原样拷进 Compiler Explorer。
侧信道里的 file 因 -fmacro-prefix-map 是相对路径(学习者看着舒服),但协议要求绝对路径(d2x 靠它开编辑器、监听变更)。在协议边界上还原成绝对。
按库设计,目前住在本仓库,将来可整体抽成独立 mcpp 包给其他 C++ 课程复用。
| 设施 | #include <d2x/cpp/common.hpp> |
import d2x.harness; |
|---|---|---|
| 断言 | d2x_assert / d2x_assert_eq 宏 |
d2x::check / check_eq,source_location 自动带 file/line |
| 路障 | D2X_WAIT |
d2x::wait() |
| 填空占位 | D2X_YOUR_ANSWER |
无等价物 |
宏无法跨模块导出,这是 C++ 的硬约束而非疏漏。D2X_YOUR_ANSWER 必须展开为空才能制造编译错误,本质就是宏。cpp20/cpp23 等模块化章节需要另设填空约定——这是个尚未决策的缺口。
include/ 下保持 d2x/cpp/common.hpp 这个既有路径,所以 104 个练习和书本一行都不用改。路径改名可以以后单独做。
d2x_assert_eq 的所有分支都必须走 d2x::detail::show(),不能用裸 std::to_string——后者没有 std::string / const char* / scoped enum 的重载,一旦有练习比较这些类型就会硬编译失败。(曾经上报分支用了 show() 而日志分支没用,是个潜伏的地雷。)
断言两件事,缺一不可:每个练习未完成时不通过、每个参考答案放进去后通过。等价于 rustlings 的 cargo dev check --require-solutions。
走的是 d2x checker 内部同一条 Provider 路径,所以绿灯等价于学习者本地能跑通,而非另一条平行路径。
两道防线:
-
防空转——
pass == 0时直接判失败。旧 CI 就是这么绿了很久的:它只挑-ref目标,而solutions/早被注释掉,循环一次都没进,job 照样退出 0;第 60 行的wc -l对空串还打印 "Found 1 reference targets" 把空转掩盖了。同类问题在本脚本自己身上也犯过一次(${sol#en/}剥离顺序错,导致所有英文答案静默 SKIP)。 -
脏树检查——练习目录有未提交改动时拒绝运行并列出文件。脚本会把参考答案覆盖到练习上再还原,天然会吃掉未提交的改动,这个陷阱咬过两次。
| 项 | 结果 |
|---|---|
| Provider 枚举 | 52 题(zh:1 + 49 + 2;en 同样 52) |
| 端到端 zh | 51/51 参考答案通过 |
| 端到端 en | 51/51 参考答案通过 |
| d2x session 单测 | 28/28 |
| 事件流 | 纯 JSON,0 污染 |
| 注入防护 | 恶意文件名被拒绝,命令未执行 |
所有练习按 c++23 编译。 mcpp 目前硬拒 c++11/14/17/20(src/manifest/types.cppm 的白名单),本方案选择不改 mcpp 上游。因此 cppNN/ 目录现在表示特性引入于哪个标准,不再改变编译参数。
04-rvalue-references 的移动构造教学点。C++17 起 prvalue 直接初始化目标(保证复制省略),-fno-elide-constructors 再也无法让 Object obj = Object(); 产生移动构造。
修法: 改成从具名对象 std::move(任何标准下都必然调用移动构造),并补一条 d2x_assert(move_ctor_calls >= 1)。
更重要的是那条断言。 漂移之所以能静默发生,正是因为从前没有任何断言检查它——输出少一行没人发现。现在它是 CI 可检测的事实。
教训:教学点如果没有断言覆盖,它的消失就是不可见的。 新增练习时应问:这一课的核心观测点,有断言在守着吗?
量化过的风险面:51 个参考答案在「原标准 vs c++23」下逐一编译并 diff 运行输出,50 个完全一致,仅此 1 个漂移。但随着课程往 cpp17/20 扩写,这类静默漂移只会变多。
.agents/skills/d2mcpp-authoring/ 已同步更新:
- 练习不再需要注册——放进
dslings/<std>/即被目录约定发现 - 新增
<std>章节只需建目录 + 改两个SUMMARY.md,无构建文件接线 - 每练习编译选项改为文件头部
// d2x:cxxflags:指令 - 验证命令改为
mcpp run -q -p d2x/buildtools/mcpp -- check <id>或整体bash d2x/buildtools/mcpp/tests/e2e.sh
| 缺口 | 影响 |
|---|---|
| 模块化练习的填空占位符无约定 | D2X_YOUR_ANSWER 是宏,跨不了模块边界;cpp20/cpp23 章节需先决策 |
| macOS / Windows 从未验证 | _popen、_putenv_s 的 #ifdef 分支全是纸面推断 |
| 新 CI 从未真跑过 | workflow 是手写的,xlings install -y 在 CI 环境能否装上 mcpp 未验证 |
diagnostics 只覆盖运行期断言 |
编译错误尚未解析成结构化诊断 |
| Provider 侧无单测 | discovery 的 id/order/chapter 推导、manifest 生成都只被端到端间接覆盖 |
hello-mcpp 无参考答案 |
e2e 只能 SKIP 它,入门练习实际未被验证 |
solutions/ 无 en 镜像 |
设计如此(共用一份),但意味着 en 专有的措辞错误无法被答案校验发现 |
{"kind":"assert","ok":false,"expr":"a == b","expected":"1","actual":"2", "file":"dslings/cpp11/xx.cpp","line":33} {"kind":"wait","file":"dslings/cpp11/xx.cpp","line":40}