Skip to content

扫描器 M1 限制:条件 import 与头单元在移植真实工程时是硬门槛(附一处诊断透传) #421

Description

@Sunrisepeak

XRGUI(一个 MSVC-first 的 C++23 模块化 GUI 库,~124k LOC / ~322 个模块接口单元)适配到 mcpp 的过程中,扫描器的两条 M1 限制是唯一需要大面积改源码的地方。想把实测数据留给你们做优先级参考,顺带记一处诊断透传。

环境:mcpp 2026.8.11.3 + gcc 16.1.0x86_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_enummodule/magic_enum.cppm#ifdef MAGIC_ENUM_USE_STD_MODULE / import std;。索引包 neargye.magic_enumscan_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions