Skip to content

clang 22: full BMI from --precompile poisons importers; -c <reduced>.pcm silently emits an object with no definitions #424

Description

@Sunrisepeak

两个互相独立的 clang 缺陷,都是在跑 bench/ 标准集(xmake + payload clang)时撞到的。
mcpp 自己两条都不受影响,但都是绕过去的,不是修好的,所以记在这里。

编译器:clang version 22.1.8 (llvm-project ca7933e47d3a3451d81e72ac174dcb5aa28b59d1),
payload xim-x-llvm/22.1.8,Linux x86_64。


先把语义摆清楚:BMI 有两种角色

clang 里同一个 .pcm 可能承担两件事,这决定了它能不能瘦:

产生方式 这个 BMI 的用途 22.1.8 实测体积
--precompile 给下游 import,并且是 codegen 的输入(clang -c x.pcm) full,20844 B
-fmodule-output= 只给下游 import,object 由源码那一次编译产出 reduced(已是默认),19276 B
--precompile -fmodules-reduced-bmi 20844 B —— 这个 flag 是空操作
--precompile -Xclang -emit-reduced-module-interface 强行让 --precompile 发 reduced 19276 B

所以 --precompile 发 full 是设计使然:那份 BMI 还要拿去生成目标文件。


缺陷 1 — full BMI 毒化下游,报错点落在 libc++ 里

--precompile 产出的 full BMI 发布给 importer 之后,一个用窄格式串的下游
TU 编不过:

.../include/c++/v1/__format/format_functions.h:99:30: error:
    call to implicitly-deleted default constructor of
    'formatter<basic_string<char>, wchar_t>'
   99 |       formatter<_Tp, _CharT> __f;

.../src/cli/cmd_xpkg.cppm:140:17: note: in instantiation of template class
    'std::__1::basic_format_string<wchar_t, std::__1::basic_string<char>...>'
  140 |     "{{\"namespace\":\"{}\",\"name\":\"{}\",\"error\":\"{}\"}}",

.../__format/formatter_string.h:139:70: note: default constructor of
    'formatter<basic_string<char>, wchar_t>' is implicitly deleted

那一行是纯窄字符的格式串,却被实例化成了 basic_format_string<wchar_t, ...>
报错点在 std 头文件里,离真因很远。

换成 reduced BMI,同一份源码、同一个编译器,编译通过。

合法的 BMI 不应该改变 importer 里的重载决议 —— 这条算 clang 的缺陷。

复现

git submodule update --init bench/projects/mcpp/mcpp-2026.8.11.3
cd bench/projects/mcpp/mcpp-2026.8.11.3
# 用 xmake 的默认形状(two_phases=true ⇒ --precompile ⇒ full BMI)
xmake f -p linux -m release --toolchain=llvm --sdk=<llvm-payload> --runtimes=c++_static
xmake build

⚠️ 已经排除的最小化尝试(别再走一遍)

两个都编译通过,即不构成复现:

  1. module a 用 GMF #include <format>,下游 import a + 窄 std::format —— 通过
  2. module aimport std; 导出一个返回 std::string 的函数,下游 import std; import a;
    加窄 std::format —— full / reduced 两种 BMI 都通过

所以触发条件比「full BMI + std::format」更窄,大概率还需要导入图里存在某个
wchar_t / std::wstring 的形态(mcpp 树里有 Windows 相关代码)。最小复现尚未做出,
目前只有上面那份真实工程的复现。


缺陷 2 — -c <reduced>.pcm 不诊断,静默产出没有定义的目标文件

这条更危险,而且有 8 行的最小复现。

// rb.cppm
export module rb;
export int busy(int n) {
    int acc = 0;
    for (int i = 0; i < n; ++i) acc += (i * 3) % 7;
    return acc;
}
export template <class T> T twice(T v) { return v + v; }
export struct Big { int a[64]; int sum() const { int s=0; for (int x : a) s+=x; return s; } };
clang++ -std=c++23 --precompile -x c++-module rb.cppm -o full.pcm
clang++ -std=c++23 --precompile -Xclang -emit-reduced-module-interface \
        -x c++-module rb.cppm -o red.pcm

clang++ -std=c++23 -c full.pcm -o from-full.o   # 退出 0
clang++ -std=c++23 -c red.pcm  -o from-red.o    # 退出 0 ← 问题在这
来源 .o 大小 定义的符号数 定义了 busy() ?
full BMI 1664 B 3
reduced BMI 1288 B 1

clang -c <reduced>.pcm 退出码 0,只是把函数定义丢了 —— 失败推迟到链接期
(undefined symbol),或者在有别处提供同名符号时根本不失败

reduced BMI 按定义不含函数体,所以它不能做 codegen 输入是对的;
但这应当被诊断,而不是静默产出一个残缺的 object。


mcpp 为什么不受影响(以及这不是「修好了」)

  • 缺陷 1:src/toolchain/model.cppm:277
    .bmiOnlyFlags = " --precompile -Xclang -emit-reduced-module-interface",
    发布给 importer 的一律是 reduced BMI。
  • 缺陷 2:src/build/schedule/policy.cppm 里明确写了 —— 两阶段的 object 边重编源码,
    绝不 -c x.pcm。前端多做一遍,实测在每个并发档位仍然净赚
    (-j4 56.3s→37.6s,-j8 34.0s→25.6s,-j32 32.0s→18.0s)。

两条都是规避。上游修好之前,任何走 --precompile → -c x.pcm 形状的构建工具
在 clang 22 上都会踩缺陷 1。


对生态的影响:这不是 xmake 的 bug

xmake 3.1.0 的 build.c++.modules.two_phases 默认 true
(core/project/policy.lua:85),该形状是 --precompile.pcm-c .pcm,
必须要 full BMI。所以 rules/c++/modules/clang/builder.lua:40

-- disabled with two phases currently, LLVM currently have a bug which prevent to emit reduced bmi when using two
local add_reduced_flag = not has_two_phases and has_reduced_bmi

拒绝把 reduced 和 two_phases 组合起来是正确的 —— 注释里那句 "LLVM bug" 指的正是
上面的缺陷 2。结论是:xmake 在 clang 22 上必然发 full BMI,因而必然触发缺陷 1,
这是上游语义逼出来的,不是它写错了。

顺带两处不影响正确性的小问题,值得给 xmake 提:

  • builder.lua:43if has_two_phases and add_reduced_flag死代码
    —— 第 40 行已保证两者不可能同真,那条 reduced.<name>.pcm 路径永远走不到。
  • support.lua:375has_flags("-fmodules-reduced-bmi") 探测是假阳性:
    clang 22.1.8 接受这个 flag 但不做任何事(见上面第一张表第三行)。

待办

  • 把缺陷 2 提到 llvm-project(有最小复现,可以直接开)
  • 缺陷 1 先做出最小复现再提;在那之前 bench 里这一格按已声明的缺口记录
  • 验证 set_policy("build.c++.modules.two_phases", false) 能否让 xmake 走
    -fmodule-output 形状从而绕开缺陷 1 —— 若可以,bench 侧就有一个受支持的配置
    而不是 hack(代价是失去 xmake 的两阶段)
  • 上游修好后回头检查 bmiOnlyFlags 与「object 边重编源码」还有没有必要

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream-bug根因在上游(编译器/构建工具/依赖),mcpp 只能绕过或声明为缺口

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions