Skip to content

Commit ead711c

Browse files
committed
The c-abi probe on a Windows host strips the host's leaked macros; the wchar realisation on a freestanding target no longer assumes the toolchain default is 32 bits (2026.9.18.3)
1 parent 9cd8638 commit ead711c

10 files changed

Lines changed: 360 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,56 @@
55

66
## [Unreleased]
77

8+
## [2026.9.18.3] - 2026-09-18
9+
10+
### Windows 主机 × freestanding 目标的 c-abi 校验探针两处真实缺陷被关掉
11+
12+
2026.9.18.1 发布的几小时内,openkal-llvm-runtime#24 的 CI 在 Windows 主机 × `riscv64-none-elf`
13+
(freestanding) 一行红——探针报两条不匹配,都是真实存在的结构性缺陷,根因都在测量一侧而
14+
不在声明一侧:
15+
16+
- `_WIN32` 声明 undefined,实测 defined。Windows 主机的 clang 即使带上 `--target=
17+
riscv64-none-elf` 仍把 `_WIN32`(及 `__MINGW*__` 一族)注入预处理器的输出——hosted
18+
三元组的 `--target=` 替换会改写主机宏,freestanding 不会。
19+
- `__SIZEOF_WCHAR_T__` 声明 32,实测 16。`cenv::realise` 之前对 freestanding 的 wchar
20+
默认值做了一个未经实测的假设:工具链默认就是 32 位,因此 `wchar = 32` 的声明在
21+
freestanding 上不补 `-fno-short-wchar`。Linux/macOS 主机下这个假设恰好成立,Windows
22+
主机下不成立——clang 用 MinGW 的 `<winnt.h>` 默认,即使 `--target=riscv64-none-elf`
23+
也给出 16 位。
24+
25+
两条都是测量侧缺陷:探针应当读到的是「引擎根据声明生成的编译命令实际产生什么」,而不是
26+
「引擎的编译命令加上主机泄漏之后产生什么」。这一版分别从两个角度修:
27+
28+
1. **引擎补 `-fno-short-wchar`(即实现逻辑的修正,不是 workaround)。** 既然「freestanding
29+
默认 32 位」的假设是错的,那么正确的策略就是不论宿主是什么、freestanding 与否,
30+
`wchar = 32` 一律加 `-fno-short-wchar`——这样编译产物确实就是 32 位 `wchar_t`,
31+
探针再去量也是 32 位,声明成立。在 Linux/macOS 主机下加这个令牌是冗余的(它们本来就
32+
是 32),但对的结果不变;在 Windows 主机下是必要的。原先的「freestanding 跳过 wchar
33+
令牌」规则由此被取消,在 `mcpp.toolchain.cenv::realise` 中注释清楚。
34+
2. **探针允许调用方剥离主机宏。** `cenv_probe::verify` 增加一个 `hostStripMacros`
35+
参数——一组 `-U<name>` 令牌,在 `-E -dM` 之前前置。Windows 主机下,调用方把这四个
36+
名(`_WIN32`、`_WIN64`、`__MINGW32__`、`__MINGW64__`)传给探针;Linux/macOS 主机下
37+
传空集合。缓存键把这些令牌一起折进去,所以同编译器、同 argv、不同剥离集合不会共享
38+
缓存槽。
39+
40+
新增 `test_cenv_probe.cpp` 中三个测试,分别钉住:剥离后宏不再出现在 dump 里、不同剥
41+
离集合不共享缓存槽、Windows 主机的剥离集合恰好是那四个实测到的名字。
42+
`test_cenv.cpp` 中新增两个测试,分别钉住:`wchar = 32` 在 freestanding 上也产出
43+
`-fno-short-wchar`,`wchar = 16` 在 freestanding 上产出 `-fshort-wchar`——把「
44+
freestanding 跳 wchar 令牌」这条曾经存在过的规则永久挡在回归测试之外。
45+
46+
包侧四处尝试均失败:CI 跳过(workaround,用户拒)、去掉 freestanding 上的 musl 依赖(破坏
47+
libcxx 的 `<__mbstate_t.h>`)、per-target `[c-abi]` override(被解析但未生效——`[target.'cfg(..)'.build]`
48+
的合法键集合是 BuildInputs 的成员,不接受 `c-abi` 块)。这一版把这两条结构性缺陷收进了
49+
引擎里,而不是把它们推到包层。
50+
51+
(`src/toolchain/cenv.cppm`、`src/toolchain/cenv_probe.cppm`、`src/build/prepare.cppm`,
52+
测试 `test_cenv.cpp`(`FreestandingWchar32AlwaysEmitsNoShortWchar`、
53+
`FreestandingWchar16AlwaysEmitsShortWchar`)与 `test_cenv_probe.cpp`(
54+
`AHostStrippedMacroIsAbsentFromTheDump`、
55+
`AStripListDoesNotShareACacheSlotWithAnEmptyStrip`、
56+
`TheWindowsHostStripListNamesExactlyTheFourMeasuredLeaks`)、`modules/versioning/src/version.cppm`、`mcpp.toml`)
57+
858
## [2026.9.18.2] - 2026-09-18
959

1060
### 2026.9.18.1 发布几小时内,三个下游仓库的 CI 揭出的三处同形缺陷:声明满足与做不到被当成了一回事

‎docs/22-target-side.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,26 @@ the build and prints both the declared and the measured values. The result
484484
is cached per configuration (compiler binary identity + exact flags), so a
485485
build that resolves the same configuration twice pays for the probe once.
486486

487+
**Host contamination (mcpp 2026.9.18.3+).** The probe runs on the BUILD
488+
host's clang, not a target-native one, and on a Windows host the driver's
489+
predefines (`_WIN32`, `_WIN64`, `__MINGW32__`, `__MINGW64__`) leak through
490+
`--target=` substitution for a freestanding target the same way the
491+
Cygwin-substituted Windows row's `__CYGWIN__` does NOT — a structural
492+
difference between the hosted and freestanding substitutions that the
493+
verification step's measurement has to compensate for. The probe accepts
494+
a `hostStripMacros` list of `-U<name>` tokens it prepends to its `-dM`
495+
command, and the caller (this layer's `prepare`) supplies exactly the four
496+
Windows-host names on Windows, nothing on Linux or macOS. The matching
497+
defect on `__SIZEOF_WCHAR_T__` (Windows host × freestanding measures 2
498+
where a `wchar = 32` declaration asks for 4) is closed on the realisation
499+
side, not the probe side: `cenv::realise` now ALWAYS emits
500+
`-fno-short-wchar` for `decl.wcharBits = 32`, regardless of what the
501+
host's toolchain would default to, so the probe measures the state the
502+
engine actually produced (32 bits, with the flag) rather than the host's
503+
leak. The "freestanding skips the wchar flag" rule the wave's earlier
504+
versions carried was an unverified assumption about the toolchain default,
505+
and the wave's measurement is what verified it wrong.
506+
487507
**Fingerprint.** The realised environment participates in the build's
488508
fingerprint (`compileFlags`, §92's field 7): two builds whose C library
489509
declares `lp64` and `llp64` compile the same source into objects whose

‎docs/zh/22-target-side.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,19 @@ c-environment = "platform"
388388
同时打印声明值与实测值。结果按配置(编译器二进制身份 + 最终参数)缓存,同一配置解析两次只
389389
编译一次探针。
390390

391+
**主机污染(mcpp 2026.9.18.3+)。** 探针跑在**构建主机**的 clang 上,而不是目标本地的;
392+
Windows 主机的驱动会预先定义 `_WIN32`、`_WIN64`、`__MINGW32__`、`__MINGW64__`,这些定义会
393+
穿透 `--target=` 替换到达 freestanding 目标——而 hosted 三元组的 `--target=` 替换是不会
394+
让 `_WIN32`/`_WIN64` 漏出来的(那是 Cygwin 那一行已经处理过的事情)。这是 hosted 与
395+
freestanding 替换之间一个结构性差异,核对步骤的测量必须为此做补偿。探针接受一个
396+
`hostStripMacros` 参数,即一组 `-U<name>` 令牌,它在 `-dM` 之前前置;调用方(本层的
397+
`prepare`)在 Windows 主机上恰好传那四个名字,Linux/macOS 主机上传空集合。`__SIZEOF_WCHAR_T__`
398+
那一半(Windows 主机 × freestanding 测出 2,而 `wchar = 32` 的声明要求 4)是在实现侧关的,
399+
不是探针侧:`cenv::realise` 现在对 `decl.wcharBits = 32` **一律**发 `-fno-short-wchar`——
400+
不论宿主工具链的默认值是什么——这样探针测量到的就是引擎实际产出的状态(32 位、令牌在),
401+
而不是主机的泄漏。早先版本里「freestanding 跳过 wchar 令牌」那条规则是一个未实测的关于
402+
工具链默认值的假设,本轮的测量把它证伪了。
403+
391404
**指纹。** 解析出的环境参与构建指纹(`compileFlags`,§92 的第 7 项):C 库声明 `lp64` 与
392405
`llp64` 的两次构建,从同一份源码编译出 `long` 宽度不同的目标文件,因此二者绝不共享输出目录,
393406
也不会复用对方产出的目标文件缓存。

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.18.2"
3+
version = "2026.9.18.3"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

‎modules/versioning/src/version.cppm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.9.18.2";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.9.18.3";
3535

3636
} // namespace mcpp

‎src/build/prepare.cppm‎

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10852,6 +10852,68 @@ prepare_build(bool print_fingerprint,
1085210852
// paths and library search flags do not, and leaving them out
1085310853
// is what makes this probe cheap AND cacheable across every
1085410854
// package that shares this build's target side.
10855+
//
10856+
// `hostStripMacros` — Windows-host leak through `--target=`
10857+
// substitution. A freestanding cross compile (`--target=
10858+
// riscv64-none-elf`) on a Windows host still sees `_WIN32` (and
10859+
// the `__MINGW*__` family) in the preprocessor output: those
10860+
// are the HOST driver's predefines, and unlike the `__APPLE__` /
10861+
// `__linux__` family on Linux/macOS, the `--target=` substitution
10862+
// does NOT strip them for the freestanding target. Without the
10863+
// strip, the probe's `-dM` dump records `_WIN32` defined
10864+
// regardless of what the realised `[c-abi] presents = "posix"`
10865+
// asked for, and the probe fails with a mismatch that is a
10866+
// property of the host's driver, not of the declaration being
10867+
// checked. The strip is added ONLY when the host is Windows;
10868+
// Linux and macOS drivers' defaults do not contaminate
10869+
// `--target=` substitutions in the same way, and adding `-U`
10870+
// tokens there would have to be defended as harmless rather
10871+
// than measured (the wave's measurement caught exactly four
10872+
// host-side leaks — listed below — and adding to that set is
10873+
// the right way to extend the strip; speculatively unstripping
10874+
// everywhere is not).
10875+
//
10876+
// `hostStripFlags` — host-side wchar leakage on Windows ×
10877+
// freestanding. The probe runs `-E -dM -x c++` with no source
10878+
// unit and no include path; the `__SIZEOF_WCHAR_T__` it reads
10879+
// comes from the toolchain's own defaults, which clang on a
10880+
// Windows host sets from `<winnt.h>` (16 bits) even with
10881+
// `--target=riscv64-none-elf`. The realisation closes this by
10882+
// ALWAYS emitting `-fno-short-wchar` for `decl.wcharBits = 32`
10883+
// (`mcpp.toolchain.cenv`'s wchar branch, just rewritten — the
10884+
// old "freestanding skips the flag" rule was an unverified
10885+
// assumption that the wave's measurement caught as false). So
10886+
// when the host is Windows AND the target is freestanding, the
10887+
// host-strip set above AND `-ffreestanding` are both needed —
10888+
// the former for the preprocessor predefines, the latter so the
10889+
// driver does not pick up the Windows CRT's `<wchar.h>` even
10890+
// when the build's own include path doesn't carry one. Outside
10891+
// that combination the strip is unnecessary: Linux/macOS hosts
10892+
// do not leak `_WIN32` through `--target=`, and the wchar fix
10893+
// lives in the realisation, not the probe.
10894+
std::vector<std::string> hostStripMacros;
10895+
std::vector<std::string> hostStripFlags;
10896+
if (mcpp::platform::is_windows) {
10897+
// The four names measured as leaking through `--target=` on
10898+
// a Windows host (2026-09-18, openkal-llvm-runtime#24,
10899+
// windows-host × riscv64-none-elf). Adding to this set is
10900+
// a measurement-driven change, not a guess; pin new entries
10901+
// here with the failing build that named them.
10902+
hostStripMacros = {
10903+
"-U_WIN32",
10904+
"-U_WIN64",
10905+
"-U__MINGW32__",
10906+
"-U__MINGW64__",
10907+
};
10908+
if (tt && tt->is_freestanding())
10909+
// Windows host's driver would otherwise pull in
10910+
// MinGW's `<wchar.h>` even with no source unit, so the
10911+
// wchar probe sees 2 instead of the declared 32.
10912+
// `-ffreestanding` is what every real compile on a
10913+
// freestanding target already adds (`openkal-musl`'s
10914+
// `cflags`, mcpp's own freestanding handling).
10915+
hostStripFlags.push_back("-ffreestanding");
10916+
}
1085510917
if (tc->cEnvExpectWcharBits != 0 || tc->cEnvExpectLongBytes != 0
1085610918
|| !tc->cEnvExpectDefined.empty()
1085710919
|| !tc->cEnvExpectUndefined.empty()) {
@@ -10860,10 +10922,12 @@ prepare_build(bool print_fingerprint,
1086010922
probeArgv.push_back(tc->crossTargetFlag);
1086110923
for (auto& t : tc->cEnvTokens) probeArgv.push_back(t);
1086210924
for (auto& t : tc->cEnvBuiltinsTokens) probeArgv.push_back(t);
10925+
for (auto& t : hostStripFlags) probeArgv.push_back(t);
1086310926
auto probe = mcpp::toolchain::cenv_probe::verify(
1086410927
tc->binaryPath, probeArgv,
1086510928
tc->cEnvExpectWcharBits, tc->cEnvExpectLongBytes,
10866-
tc->cEnvExpectDefined, tc->cEnvExpectUndefined);
10929+
tc->cEnvExpectDefined, tc->cEnvExpectUndefined,
10930+
hostStripMacros);
1086710931
if (!probe) {
1086810932
refusal::record(refusal::Code::CEnvUnrealisable);
1086910933
return std::unexpected(probe.error());

‎src/toolchain/cenv.cppm‎

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,19 @@ inline mcpp::targetside::CAbiDataModel triple_native_data_model(
186186
// The `wchar_t` width a target's default triple already has. Measured
187187
// (design §1.4): 16 on Windows (MinGW and Cygwin alike, absent
188188
// `-fno-short-wchar`), 32 everywhere else clang targets (Linux, macOS,
189-
// freestanding ELF).
189+
// hosted ELF/Mach-O).
190+
//
191+
// WHAT IS NOT IN THIS FUNCTION. The freestanding case used to be answered
192+
// "32", on the reasoning that `riscv64-none-elf` etc. measure as 32 bits on
193+
// Linux and macOS hosts. The wave's Windows-host × riscv64-none-elf
194+
// measurement showed that assumption to be wrong: clang on a Windows host
195+
// still uses MinGW's `<winnt.h>` defaults even with `--target=riscv64-none-elf`,
196+
// and `__SIZEOF_WCHAR_T__` measures 2 (16 bits) there. The probe correctly
197+
// flagged this as a mismatch with `decl.wcharBits = 32`. The fix is in the
198+
// `wchar` realisation below — always add `-fno-short-wchar` when the
199+
// declaration asks for 32, regardless of what the toolchain would have
200+
// defaulted to — so the probe then measures the state the engine actually
201+
// produced, not the host's leak.
190202
inline int native_wchar_bits(std::string_view os) {
191203
return os == "windows" ? 16 : 32;
192204
}
@@ -362,14 +374,31 @@ inline std::expected<Realisation, std::string> realise(
362374
}
363375

364376
// ── `wchar` ──────────────────────────────────────────────────────────────
377+
//
378+
// The freestanding case used to be "assumed native, no token added".
379+
// The wave's Windows-host × riscv64-none-elf measurement (openkal-
380+
// llvm-runtime#24, 2026-09-18) caught that assumption as wrong:
381+
// clang on a Windows host uses MinGW's `<winnt.h>` defaults even with
382+
// `--target=riscv64-none-elf`, so the toolchain's own default for
383+
// `wchar_t` is 16 bits there — and a `decl.wcharBits = 32` (musl's
384+
// declaration) would compile to a 16-bit `wchar_t` because no token
385+
// was added. The probe caught this; the right fix is here, not in the
386+
// probe: always emit `-fno-short-wchar` when the declaration asks for
387+
// 32, so the compiler produces 32-bit `wchar_t` regardless of what its
388+
// host-contaminated default would have been. Same for `wchar=16` on
389+
// a target that would otherwise default to 32: `-fshort-wchar` keeps
390+
// the compile honest.
391+
//
392+
// The probe below then measures what this engine actually produced
393+
// (32 bits, with the flag) — not the host's leak — and the declaration
394+
// holds. There is no measurement-side workaround here: the previous
395+
// version had a "freestanding target skips the wchar flag" rule that
396+
// was wrong on Windows hosts, and removing it costs nothing on
397+
// Linux/macOS hosts (they default to 32, so adding the flag there is
398+
// redundant but harmless).
365399
if (decl.hasWchar) {
366-
const int native = freestanding ? 32 : native_wchar_bits(os);
400+
const int native = native_wchar_bits(os);
367401
if (decl.wcharBits != native) {
368-
// One clang pair, relative to whatever the triple in force
369-
// (possibly already substituted above) would otherwise give:
370-
// `-fshort-wchar` for 16, `-fno-short-wchar` for 32. Measured on
371-
// the Cygwin triple (design §1.4): default 16, `-fno-short-wchar`
372-
// gives 32.
373402
r.tokens.push_back(decl.wcharBits == 32 ? "-fno-short-wchar"
374403
: "-fshort-wchar");
375404
}

0 commit comments

Comments
 (0)