把 XRGUI(一个 MSVC-first 的 C++23 模块化 GUI 库,~124k LOC / ~322 个模块接口单元)适配到 mcpp 的过程中,扫描器的两条 M1 限制是唯一需要大面积改源码的地方。想把实测数据留给你们做优先级参考,顺带记一处诊断透传。
环境:mcpp 2026.8.11.3 + gcc 16.1.0,x86_64-linux-gnu。
1. 条件 import 被拒 — 命中 17 处
error: import statement inside conditional preprocessor block (forbidden in M1)
src/modgraph/scanner.cppm:660。
命中的几乎全是同一个模式 —— 一个宏在两种拿到同一批第三方头的方式之间切换:
module;
#ifndef XRGUI_FUCK_MSVC_INCLUDE_CPP_HEADER_IN_MODULE
#include "plf_hive.h"
#include <gtl/phmap.hpp>
#endif
export module mo_yanxi.graphic.image_atlas;
import std;
#ifdef XRGUI_FUCK_MSVC_INCLUDE_CPP_HEADER_IN_MODULE
import <plf_hive.h>;
import <gtl/phmap.hpp>;
#endif
这个宏从未在 mcpp 侧定义,所以那条 import 是死代码 —— 但扫描是纯词法的,照样拒。
同类还有两处形态不同的:
neargye/magic_enum 的 module/magic_enum.cppm:#ifdef MAGIC_ENUM_USE_STD_MODULE / import std;。索引包 neargye.magic_enum 靠 scan_overrides 绕过了,但直接 vendor 上游 submodule 的人会撞上。
- 一个实现单元把
import 写在 #if defined(__cpp_lib_stacktrace) 里(这处确实该修 —— import 本来就该在 purview 开头)。
观察:docs/zh/05-mcpp-toml.md §2.3 写了 [build].defines "会进入 P1689 模块扫描 —— 这正是被宏保护的 import 能被解析的前提"。但实际上扫描器在看到条件块里有 import 就直接报错,不看条件是否可判定。文档描述与实现有出入,至少值得对齐一下措辞。
一个可能的中间档:如果条件只依赖 [build].defines / [targets.*].defines 里出现过的宏,扫描器其实有足够信息判定,可以求值而不是拒绝。真正不可判定的(依赖工具链内建宏如 __cpp_lib_stacktrace)再报错。
2. 头单元被拒 — 与 1 是同一批文件
error: header units (import "h" / import <h>) are forbidden in M1
src/modgraph/scanner.cppm:666。
两条加起来的效果是:MSVC 工程里"用头单元吃第三方头"这条路在 mcpp 下没有任何保留余地。我最后只能把 16 个文件的 #include 分支改成无条件、把头单元分支整个删掉。
这个改动会影响该工程的 MSVC 构建行为(xmake 那边仍然定义着那个宏),所以对一个"只想加一套 mcpp 构建、不动原有构建"的工程来说,这是适配成本里最扎眼的一项。不是说 M1 的取舍不对 —— 头单元本身就是个泥潭 —— 只是想让你们知道它在真实工程上的实际代价。
3. 一处诊断透传:私有模块片段
module : private; 会得到:
font.ixx:703:1: error: module already declared
703 | module : private;
| ^~~~~~
这是 GCC 的错误,不是 mcpp 的 —— 我一开始归因错了,用 5 行文件逐字复现 mcpp 的扫描命令后确认:
$ cat m.ixx
export module m;
export int f();
module : private;
int helper() { return 1; }
int f() { return helper(); }
$ g++ -std=c++23 -fmodules -c m.ixx
m.ixx:3:1: sorry, unimplemented: private module fragment ← 清楚
$ g++ -std=c++23 -fmodules -fdeps-format=p1689r5 -fdeps-file=m.ddi \
-fdeps-target=m.o -M -MM -MF m.ddi.dep -x c++ -E m.ixx -o m.o
m.ixx:3:1: error: module already declared ← 误导
同一件事(GCC 16 没实现私有模块片段),编译路径给的是 sorry, unimplemented,P1689 扫描路径给的是 module already declared。用户看到后者只会去找"重复的 module 声明",找不到。
不是要 mcpp 修 GCC,只是:既然扫描器已经在做词法解析,识别出 module : private; 并给一句"当前工具链未实现私有模块片段(GCC 16)"的话,能省掉一轮误导性排查。优先级应该很低。
(顺带:我那个工程里这处用法本身也不合规 —— 带私有片段的模块单元必须是该模块唯一的模块单元,而 mo_yanxi.font 还有一个实现单元。IFNDR,所以谁都没义务报。)
其余部分的体验
想说清楚上面三条是全部的摩擦点 —— 这次适配里 mcpp 侧其他一切都按预期工作,包括几个我事先专门验证过的载荷性假设:
module_extensions = [".ixx"] 让默认 glob 自动变宽,一行搞定 MSVC 拼法
- path 依赖包里的任意模块,消费者可直接
import,不需要 lib-root、不需要在根模块 re-export
- 无 lib-root 的"模块袋"能作为
kind = "lib"
- path 依赖的
include_dirs 正确传播给消费者
- 多个
[targets.*] bin 共享同一批 object(compile-once 模型),各自只有自己的入口 .o
sources glob 可以用 ../ 越出包根 —— 这条让我能给三个第三方 submodule 写"薄包",零上游改动就把它们变成 path 依赖
[features] + [feature-deps] 把 gtest 完全挡在默认构建之外(feature 不激活时根本不下载)
126k LOC 的模块依赖图一次扫对,没有一处图错误。
适配的完整记录(含 GCC vs MSVC 的源码符合性差异清单)在
Sunrisepeak/xrgui#1
把 XRGUI(一个 MSVC-first 的 C++23 模块化 GUI 库,~124k LOC / ~322 个模块接口单元)适配到 mcpp 的过程中,扫描器的两条 M1 限制是唯一需要大面积改源码的地方。想把实测数据留给你们做优先级参考,顺带记一处诊断透传。
环境:mcpp
2026.8.11.3+ gcc16.1.0,x86_64-linux-gnu。1. 条件 import 被拒 — 命中 17 处
src/modgraph/scanner.cppm:660。命中的几乎全是同一个模式 —— 一个宏在两种拿到同一批第三方头的方式之间切换:
这个宏从未在 mcpp 侧定义,所以那条
import是死代码 —— 但扫描是纯词法的,照样拒。同类还有两处形态不同的:
neargye/magic_enum的module/magic_enum.cppm:#ifdef MAGIC_ENUM_USE_STD_MODULE / import std;。索引包neargye.magic_enum靠scan_overrides绕过了,但直接 vendor 上游 submodule 的人会撞上。import写在#if defined(__cpp_lib_stacktrace)里(这处确实该修 —— import 本来就该在 purview 开头)。观察:
docs/zh/05-mcpp-toml.md§2.3 写了[build].defines"会进入 P1689 模块扫描 —— 这正是被宏保护的import能被解析的前提"。但实际上扫描器在看到条件块里有 import 就直接报错,不看条件是否可判定。文档描述与实现有出入,至少值得对齐一下措辞。一个可能的中间档:如果条件只依赖
[build].defines/[targets.*].defines里出现过的宏,扫描器其实有足够信息判定,可以求值而不是拒绝。真正不可判定的(依赖工具链内建宏如__cpp_lib_stacktrace)再报错。2. 头单元被拒 — 与 1 是同一批文件
src/modgraph/scanner.cppm:666。两条加起来的效果是:MSVC 工程里"用头单元吃第三方头"这条路在 mcpp 下没有任何保留余地。我最后只能把 16 个文件的
#include分支改成无条件、把头单元分支整个删掉。这个改动会影响该工程的 MSVC 构建行为(xmake 那边仍然定义着那个宏),所以对一个"只想加一套 mcpp 构建、不动原有构建"的工程来说,这是适配成本里最扎眼的一项。不是说 M1 的取舍不对 —— 头单元本身就是个泥潭 —— 只是想让你们知道它在真实工程上的实际代价。
3. 一处诊断透传:私有模块片段
module : private;会得到:这是 GCC 的错误,不是 mcpp 的 —— 我一开始归因错了,用 5 行文件逐字复现 mcpp 的扫描命令后确认:
同一件事(GCC 16 没实现私有模块片段),编译路径给的是
sorry, unimplemented,P1689 扫描路径给的是module already declared。用户看到后者只会去找"重复的 module 声明",找不到。不是要 mcpp 修 GCC,只是:既然扫描器已经在做词法解析,识别出
module : private;并给一句"当前工具链未实现私有模块片段(GCC 16)"的话,能省掉一轮误导性排查。优先级应该很低。(顺带:我那个工程里这处用法本身也不合规 —— 带私有片段的模块单元必须是该模块唯一的模块单元,而
mo_yanxi.font还有一个实现单元。IFNDR,所以谁都没义务报。)其余部分的体验
想说清楚上面三条是全部的摩擦点 —— 这次适配里 mcpp 侧其他一切都按预期工作,包括几个我事先专门验证过的载荷性假设:
module_extensions = [".ixx"]让默认 glob 自动变宽,一行搞定 MSVC 拼法import,不需要 lib-root、不需要在根模块 re-exportkind = "lib"include_dirs正确传播给消费者[targets.*]bin 共享同一批 object(compile-once 模型),各自只有自己的入口.osourcesglob 可以用../越出包根 —— 这条让我能给三个第三方 submodule 写"薄包",零上游改动就把它们变成 path 依赖[features]+[feature-deps]把 gtest 完全挡在默认构建之外(feature 不激活时根本不下载)126k LOC 的模块依赖图一次扫对,没有一处图错误。
适配的完整记录(含 GCC vs MSVC 的源码符合性差异清单)在
Sunrisepeak/xrgui#1