Skip to content

Commit 7788d3e

Browse files
Windows-host c-abi probe strips host predefines; freestanding wchar realisation always emits -fno-short-wchar (2026.9.18.3) (#673)
* 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) * fix cenv: the freestanding branch must emit -fno-short-wchar unconditionally bb0437d dropped the freestanding ? 32 : native_wchar_bits(os) special case that 7097acd had added, on the grounds that it was wrong on Windows hosts. That is right for the -U_<WIN32>-stripped probe, but for the REAL compile (the one whose cflags the engine broadcasts to the package's own translation units) the toolchain's freestanding default on Windows is host-contaminated: clang on Windows emits a 16-bit wchar_t by default for -target=riscv64-none-elf unless told otherwise. So a declaration of wchar=32 needs -fno-short-wchar on every freestanding target regardless of what native_wchar_bits(os) says, and the previous "if (decl.wcharBits != native)" gate lets a 32-bit declaration through to a 16-bit compile on Windows freestanding. Symmetric for wchar=16: a freestanding Linux/macOS host's default wchar is 32, and the declaration 16 needs -fshort-wchar. The "hosted" branch (the else-if) keeps the old behaviour; only the freestanding branch is restructured. Verified locally with openkal-musl#37 and openkal-llvm-runtime#24's matrix against mcpp built from this branch. * cenv_probe: cacheRoot before hostStripMacros; presents=none emits -fno-short-wchar The previous parameter order put hostStripMacros before cacheRoot. That meant callers passing a custom cache directory (the test suite does this in every TEST) had to also pass an empty hostStripMacros, even though they did not care about the strip. Reordering puts cacheRoot first so the strip's default value (`{}`) is what callers that override cache actually mean, and tests that want to assert the strip's behaviour pass both. prepare.cppm's call passes both, in the reordered order. The "presents = none" wchar branch used to assert tokens.empty(). That is wrong under the new freestanding-wchar rule: -fno-short-wchar is emitted regardless of presents, because the wchar realisation is about width (the toolchain's host-contaminated default), not about identity (what macros the preprocessor states). Updated to assert the flag's presence and the absence of any identity macros. * test_cenv_probe: pin the strip parameter end-to-end against a name no host predefines * test: re-trigger CI --------- Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com> Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 9cd8638 commit 7788d3e

10 files changed

Lines changed: 404 additions & 15 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: 66 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,13 @@ 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+
mcpp::home::cache_root(),
10931+
hostStripMacros);
1086710932
if (!probe) {
1086810933
refusal::record(refusal::Code::CEnvUnrealisable);
1086910934
return std::unexpected(probe.error());

src/toolchain/cenv.cppm

Lines changed: 51 additions & 8 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,45 @@ 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: a freestanding target ALWAYS gets `-fno-short-wchar` for
387+
// `wchar=32`, so the compiler produces 32-bit `wchar_t` regardless of
388+
// what its host-contaminated default would have been. Symmetric for
389+
// `wchar=16`: a freestanding target's toolchain default on Linux/macOS
390+
// is 32, so `-fshort-wchar` is always needed too. The hosted cases
391+
// are unchanged — `native_wchar_bits(os)` is correct for every hosted
392+
// target, because the host's toolchain default is what the real
393+
// compile actually sees (Windows host has `-fshort-wchar` baked in
394+
// via MinGW headers; Linux/macOS hosts have 32 bits).
395+
//
396+
// The probe below then measures what this engine actually produced
397+
// (32 bits on a freestanding target, with the flag) — not the host's
398+
// leak — and the declaration holds. There is no measurement-side
399+
// workaround here: the previous version had a "freestanding target
400+
// skips the wchar flag" rule that was wrong on Windows hosts, and
401+
// adding it costs nothing on Linux/macOS hosts (they default to 32,
402+
// so the flag is redundant but harmless on the hosted freestanding
403+
// builds that don't exist; for hosted Linux/macOS, no flag is added).
365404
if (decl.hasWchar) {
366-
const int native = freestanding ? 32 : native_wchar_bits(os);
367-
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.
405+
if (freestanding) {
406+
// Freestanding: the toolchain's host-contaminated default is
407+
// not something this engine can trust. Always emit the
408+
// token that makes the compile match the declaration.
409+
r.tokens.push_back(decl.wcharBits == 32 ? "-fno-short-wchar"
410+
: "-fshort-wchar");
411+
} else if (decl.wcharBits != native_wchar_bits(os)) {
412+
// Hosted: the target's own default is what the compile sees,
413+
// and `native_wchar_bits(os)` correctly captures it
414+
// (16 on Windows because MinGW headers bake in
415+
// `-fshort-wchar`; 32 on Linux and macOS).
373416
r.tokens.push_back(decl.wcharBits == 32 ? "-fno-short-wchar"
374417
: "-fshort-wchar");
375418
}

0 commit comments

Comments
 (0)