Skip to content

Commit affcb48

Browse files
committed
The realised [c-abi] environment reaches assembly units too
A .S unit's command line is assembled independently of a .c/.cpp unit's (mcpp.build.flags::CompileFlags::as, not ::cc/::cxx), and only inherits the -D/-U/-I subset of a package's C flags on purpose -- a -std= or -O token meant for the C compiler is meaningless to GAS. The realised environment broadcast landed only in privateBuild.cflags/cxxflags, so it never reached that narrower channel: within one package a .c unit saw _WIN32 undefined while a .S unit still saw it defined, because --target=/-fno-short-wchar never reached the assembler's command line at all. Found by the openkal-musl spike (okm_setjmp.S, and upstream libunwind's assembly.h, both select register-save sets on that macro) -- a jmp_buf written by one save set and sized by the other header is a silent mismatch, the exact failure class this feature exists to remove. Adds UsageRequirements::asmflags, a broadcast-only channel parallel to cflags/cxxflags (no [build] asmflags manifest key backs it -- per-glob `flags = [{ asmflags }]` remains the author-facing one), and routes the realised environment tokens into it for GAS units only: NASM has no --target= concept and accepts none of these flags, so it must never receive them, and a unit test pins that split directly. Every token was checked against clang's assembler-with-cpp front end first; none is rejected, so nothing is filtered a second time. e2e 741 gains a .S unit asserting it carries the same tokens main.cpp does.
1 parent f19911f commit affcb48

7 files changed

Lines changed: 204 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@
4646
让它们的出现直接失败构建。(`src/build/prepare.cppm`,`docs/06`)
4747
- **指纹**:解析出的环境与 `__openkal__` 参与构建指纹,LP64 与 LLP64 两次构建绝不共享输出
4848
目录。安装钩子的存储键尚未补上同一个缺口,已在 `docs/22` 记录为已知差距。
49+
- **实现同样到达汇编单元,经 openkal-musl 尖峰实验发现并修订。** 最初的广播只写入每个包的
50+
`privateBuild.cflags`/`cxxflags`,`.S` 单元走独立组装的 `f.as`,只从 `packageCflags` 里
51+
继承 `-D`/`-U`/`-I` 子集(`unit_asm_flags`,本就如此,为了不让 `-std=`/`-O` 这类对汇编
52+
无意义的标志混进去)——`--target=`/`-fno-short-wchar` 因此从未到达汇编器,同一个包里
53+
`.c` 单元看到 `_WIN32` 未定义而 `.S` 单元仍看到它已定义(openkal-musl 自己的
54+
`okm_setjmp.S`、上游 libunwind 的 `assembly.h` 都按这个宏选目标文件格式分支与寄存器保存
55+
集,后果是用 SysV 保存集写、按 Win64 头部量的 `jmp_buf` 悄悄错位)。新增
56+
`UsageRequirements::asmflags`——`privateBuild` 内与 `cflags`/`cxxflags` 平行、但只供引擎
57+
自己广播用的第三条通道(没有对应的 `[build] asmflags = [...]` 清单键)——把同一份令牌
58+
原样送进 `packageAsmflags`,绕开 D/U/I 过滤。实测 clang 对 `-x assembler-with-cpp` 接受
59+
这些令牌全集,故未作裁剪。(`src/modgraph/scanner.cppm`、`src/build/prepare.cppm`,e2e
60+
`tests/e2e/741_...sh` 新增 `.S` 单元与其上的断言)
4961
- 文档:`docs/22`(`[c-abi]`、校验、指纹)、`docs/21`(声明的环境如何移动编译三元组而不
5062
移动链接三元组)、`docs/24`(三组宏、`__openkal__` 的规则、平台单元)、`docs/06`
5163
(`platform-sdk` 标记)及对应 zh 镜像。

docs/22-target-side.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,8 +344,19 @@ this block, mcpp turns the request into compiler configuration for every
344344
target-side unit — the C library itself, the C++ runtime, the compiler
345345
runtime's builtins, and every ordinary package in the graph — covering C,
346346
C++ and assembly compiles, the dependency scan, and the `std` module
347-
precompile alike. mcpp holds one mapping table from request to triple and
348-
flags, generic knowledge that names no C library:
347+
precompile alike. Assembly (`.S`/`.s`) needs its own broadcast channel to get
348+
this: a `.S` unit's command line is assembled independently of a `.c`/`.cpp`
349+
unit's (`mcpp.build.flags::CompileFlags::as`, not `::cc`/`::cxx`), and only
350+
takes the `-D`/`-U`/`-I` words out of a package's C flags on purpose — a
351+
`-std=` or `-O` token meant for the C compiler is meaningless to GAS — so the
352+
realised environment tokens (`--target=`, `-f[no-]short-wchar`, and anything
353+
`builtins = "iso"` adds) are broadcast a second time, verbatim, into that
354+
narrower channel (found missing, and fixed, by the openkal-musl spike: a
355+
`.c` unit in a package saw `_WIN32` undefined while a `.S` unit in the SAME
356+
package — real code, like `okm_setjmp.S` and upstream libunwind's
357+
`assembly.h`, selects register-save sets on it — still saw it defined). mcpp
358+
holds one mapping table from request to triple and flags, generic knowledge
359+
that names no C library:
349360

350361
| Target | Request | Realisation |
351362
|---|---|---|

docs/zh/22-target-side.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,14 @@ builtins = "iso" # iso | platform(默认 platform)
285285

286286
**实现(realisation)。** 一旦 `c-abi` 层解析到声明了这个块的包,mcpp 就把请求转换成编译器
287287
配置,作用于目标侧的每一个编译单元——C 库自己、C++ 运行时、编译器运行时的 builtins,以及
288-
图中所有普通包——覆盖 C、C++、汇编编译,依赖扫描,以及 `std` 模块预编译。mcpp 保存的是一份
288+
图中所有普通包——覆盖 C、C++、汇编编译,依赖扫描,以及 `std` 模块预编译。汇编(`.S`/`.s`)要
289+
做到这一点需要自己单独的广播通道:`.S` 单元的命令行是独立组装的(`mcpp.build.flags::
290+
CompileFlags::as`,不是 `::cc`/`::cxx`),而且它有意只从包的 C 标志里取出 `-D`/`-U`/`-I`
291+
子集——对 C 编译器有意义的 `-std=``-O` 之类标志对 GAS 毫无意义——所以实现出来的环境
292+
令牌(`--target=``-f[no-]short-wchar`,以及 `builtins = "iso"` 添加的部分)要原样再广播
293+
一遍进这条更窄的通道(openkal-musl 尖峰实验发现并修好的缺口:同一个包里 `.c` 单元看到
294+
`_WIN32` 未定义,`.S` 单元却仍看到它已定义——真实代码,比如 `okm_setjmp.S` 与上游
295+
libunwind 的 `assembly.h`,正是按这个宏来选寄存器保存集的)。mcpp 保存的是一份
289296
「请求到三元组与开关」的映射表,是不含包名的通用知识:
290297

291298
| 目标 | 请求 | 实现 |

src/build/prepare.cppm

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11039,6 +11039,27 @@ prepare_build(bool print_fingerprint,
1103911039
// line these tokens are appended to is untouched either way, which is
1104011040
// what keeps a package that declares neither field byte-identical to
1104111041
// a build before this feature existed.
11042+
//
11043+
// ALSO INTO `privateBuild.asmflags` (openkal-musl spike, post-review):
11044+
// the environment is a property of the TARGET, so it has to reach
11045+
// every translation unit built for that target, assembly (.S/.s)
11046+
// included — assembly is preprocessed with the same macros, and real
11047+
// code selects on them (openkal-musl's own `okm_setjmp.S`; upstream
11048+
// libunwind's `assembly.h`). `cflags`/`cxxflags` do not reach a .S
11049+
// file wholesale (`mcpp.build.compile_commands::unit_asm_flags` keeps
11050+
// only their -D/-U/-I words, on purpose — a -std= or -O token meant
11051+
// for the C compiler has no meaning for GAS), so the object-format
11052+
// and wchar-width tokens have to be named again here, into the
11053+
// channel `unit_asm_flags` passes through UNFILTERED. `__openkal__`
11054+
// needs no second copy: it is a -D, and the -D/-U/-I filter already
11055+
// carries it from `cflags` into every assembly unit.
11056+
//
11057+
// Every token in `cEnvTokens`/`cEnvBuiltinsTokens` was checked against
11058+
// clang's GAS (`-x assembler-with-cpp`) front end before this was
11059+
// written (`--target=`, `-f[no-]short-wchar`,
11060+
// `-fno-builtin-memset_pattern16`) and none is rejected — so nothing
11061+
// here is filtered a second time; if a future token IS GAS-hostile,
11062+
// `cenv::realise` is where to split it, not this broadcast.
1104211063
if (tc && (tc->kernelAbiIsOpenkal || !tc->cEnvTokens.empty()
1104311064
|| !tc->cEnvBuiltinsTokens.empty())) {
1104411065
static const std::vector<std::string> kOpenkalDefine = {"-D__openkal__"};
@@ -11050,8 +11071,10 @@ prepare_build(bool print_fingerprint,
1105011071
if (p.manifest.cEnvironment == "platform") continue;
1105111072
appendUniqueFlags(p.privateBuild.cflags, tc->cEnvTokens);
1105211073
appendUniqueFlags(p.privateBuild.cxxflags, tc->cEnvTokens);
11074+
appendUniqueFlags(p.privateBuild.asmflags, tc->cEnvTokens);
1105311075
appendUniqueFlags(p.privateBuild.cflags, tc->cEnvBuiltinsTokens);
1105411076
appendUniqueFlags(p.privateBuild.cxxflags, tc->cEnvBuiltinsTokens);
11077+
appendUniqueFlags(p.privateBuild.asmflags, tc->cEnvBuiltinsTokens);
1105511078
}
1105611079
}
1105711080

src/modgraph/scanner.cppm

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ struct UsageRequirements {
121121
std::vector<std::filesystem::path> includeDirsAfter;
122122
std::vector<std::string> cflags;
123123
std::vector<std::string> cxxflags;
124+
// Whole-package assembler flags. There is no `[build] asmflags = [...]`
125+
// manifest key to back this — a per-glob `flags = [{ glob, asmflags }]`
126+
// entry is the author-facing channel (scan_one_into applies it below, via
127+
// apply_glob_flags) — this field exists only for the engine's own
128+
// broadcasts, today the realised [c-abi] environment tokens (design
129+
// 2026-09-18 §3.2). `cflags`/`cxxflags` do NOT reach assembly wholesale:
130+
// `mcpp.build.compile_commands::unit_asm_flags` narrows a unit's C flags
131+
// to their -D/-U/-I words before letting them near a .S file (a -std= or
132+
// -O flag meant for the C compiler is meaningless, or worse, to GAS), so
133+
// a broadcast that must reach assembly UNFILTERED — `--target=`,
134+
// `-f[no-]short-wchar`, the object-format identity itself — has to name
135+
// itself here rather than ride along in cflags and be quietly dropped.
136+
std::vector<std::string> asmflags;
124137
std::vector<std::string> ldflags;
125138
std::vector<std::string> modules;
126139
};
@@ -1175,7 +1188,8 @@ void scan_one_into(ScanResult& result,
11751188
const std::vector<std::filesystem::path>& localIncludeDirs,
11761189
const std::vector<std::filesystem::path>& localIncludeDirsAfter,
11771190
const std::vector<std::string>& packageCflags,
1178-
const std::vector<std::string>& packageCxxflags)
1191+
const std::vector<std::string>& packageCxxflags,
1192+
const std::vector<std::string>& packageAsmflags = {})
11791193
{
11801194
// This package's own extension table. Built once per package, not per
11811195
// file, and taken from THIS manifest — a dependency is classified by its
@@ -1286,6 +1300,16 @@ void scan_one_into(ScanResult& result,
12861300
u.localIncludeDirsAfter = localIncludeDirsAfter;
12871301
u.packageCflags = packageCflags;
12881302
u.packageCxxflags = packageCxxflags;
1303+
// GAS ONLY. NASM has no `--target=` concept and does not
1304+
// recognize clang-family flags at all — `packageAsmflags` here is
1305+
// the engine's own broadcast (today: the realised [c-abi]
1306+
// environment), and unlike a per-glob `flags = [{asmflags}]`
1307+
// entry (applied below by apply_glob_flags, to both kinds, which
1308+
// is safe because THAT list is author-written per glob) it is not
1309+
// written with any one assembler in mind. A NASM unit still gets
1310+
// its own per-glob asmflags untouched.
1311+
if (u.kind == mcpp::SourceKind::GasAsm)
1312+
u.packageAsmflags = packageAsmflags;
12891313
apply_glob_flags(u);
12901314
normalize_include_flags(root, u.packageCflags);
12911315
normalize_include_flags(root, u.packageCxxflags);
@@ -1305,6 +1329,9 @@ void scan_one_into(ScanResult& result,
13051329
r->localIncludeDirsAfter = localIncludeDirsAfter;
13061330
r->packageCflags = packageCflags;
13071331
r->packageCxxflags = packageCxxflags;
1332+
// See the identical GAS-only gate in the scan_overrides branch above.
1333+
if (r->kind == mcpp::SourceKind::GasAsm)
1334+
r->packageAsmflags = packageAsmflags;
13081335
apply_glob_flags(*r);
13091336
normalize_include_flags(root, r->packageCflags);
13101337
normalize_include_flags(root, r->packageCxxflags);
@@ -1451,8 +1478,21 @@ ScanResult scan_packages(const std::vector<PackageRoot>& packages) {
14511478
auto const& packageCxxflags = p.usageResolved
14521479
? p.privateBuild.cxxflags
14531480
: p.manifest.buildConfig.cxxflags;
1481+
// No `!p.usageResolved` fallback here: unlike cflags/cxxflags there is
1482+
// no `manifest.buildConfig.asmflags` to fall back to (see
1483+
// `UsageRequirements::asmflags`'s own comment) — a package's whole-
1484+
// package asm broadcast exists only once usage is resolved. A static
1485+
// empty vector (rather than a ternary mixing an lvalue with a
1486+
// temporary) keeps the resolved branch a reference, not a copy — the
1487+
// same reason packageCflags/packageCxxflags above are two same-typed
1488+
// lvalues rather than one lvalue and one temporary.
1489+
static const std::vector<std::string> kNoAsmFlags;
1490+
auto const& packageAsmflags = p.usageResolved
1491+
? p.privateBuild.asmflags
1492+
: kNoAsmFlags;
14541493
scan_one_into(result, p.root, p.manifest, localIncludeDirs,
1455-
localIncludeDirsAfter, packageCflags, packageCxxflags);
1494+
localIncludeDirsAfter, packageCflags, packageCxxflags,
1495+
packageAsmflags);
14561496
}
14571497
resolve_graph(result);
14581498
return result;
@@ -1497,6 +1537,9 @@ ScanResult scan_packages_p1689(const std::vector<PackageRoot>& packages,
14971537
r->packageCxxflags = p.usageResolved
14981538
? p.privateBuild.cxxflags
14991539
: p.manifest.buildConfig.cxxflags;
1540+
// GAS only — see the identical gate in scan_one_into above.
1541+
if (p.usageResolved && r->kind == mcpp::SourceKind::GasAsm)
1542+
r->packageAsmflags = p.privateBuild.asmflags;
15001543
result.graph.units.push_back(std::move(*r));
15011544
}
15021545
}

tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
# a provider's block actually reaches the resolved target side, that the
1515
# realised tokens reach the compile database of an ordinary package, that a
1616
# package opting out with `c-environment = "platform"` does not receive them,
17-
# and that a request this engine cannot realise is refused before anything
18-
# compiles -- rather than compiled wrong and shipped.
17+
# that a GAS (.S) unit gets the SAME tokens a .c/.cpp unit in the same package
18+
# does (a defect found by the openkal-musl spike after this test's first
19+
# version: the substitution reached `f.cc`/`f.cxx` but not `f.as`, so a .c unit
20+
# saw `_WIN32` undefined while a .S unit in the same package still saw it
21+
# defined), and that a request this engine cannot realise is refused before
22+
# anything compiles -- rather than compiled wrong and shipped.
1923
#
2024
# `x86_64-windows-gnu` is the flagship target (design's own motivating case),
2125
# and this test compiles only -- it never links or runs the artifact, so it
@@ -46,6 +50,18 @@ cat > src/main.cpp <<'EOF'
4650
int main() { return 0; }
4751
EOF
4852

53+
# A third-party-style assembly unit, mirroring the class of file the
54+
# coordinator's report named (openkal-musl's `okm_setjmp.S`, upstream
55+
# libunwind's `assembly.h`): real GAS source that is preprocessed and reads
56+
# the SAME environment macros a `.c`/`.cpp` unit does. Compiled only (never
57+
# linked/run), so it needs no runtime -- one label is enough to be valid GAS.
58+
cat > src/probe.S <<'EOF'
59+
.text
60+
.globl cabi_probe_asm_marker
61+
cabi_probe_asm_marker:
62+
ret
63+
EOF
64+
4965
cat > mcpp.toml <<'EOF'
5066
[package]
5167
name = "cabi-probe"
@@ -155,6 +171,39 @@ if "--target=x86_64-pc-cygwin" not in libc:
155171
print(f"FAIL: the c-abi provider's own unit is missing the realised triple\n args: {libc}")
156172
sys.exit(1)
157173
174+
# The GAS (.S) unit must carry the SAME environment tokens as the C/C++ units
175+
# of the SAME package -- the defect this leg pins (coordinator report,
176+
# openkal-musl spike): the substitution used to reach C/C++ compiles only, so
177+
# a `.c` unit in a package saw `_WIN32` undefined while a `.S` unit in the
178+
# SAME package still saw it defined, because `--target=`/`-fno-short-wchar`
179+
# never reached the assembler's command line at all. `--target=` is asserted
180+
# by full-string match (assembly's flag string is independently assembled --
181+
# `mcpp.build.flags::f.as`, not `f.cc` -- so a match here proves the token
182+
# actually reached that channel, not merely that it exists somewhere in the
183+
# database). `-fno-short-wchar` is meaningless to GAS (no wchar_t in
184+
# assembly) and is asserted too regardless, on the coordinator's own
185+
# instruction: "whatever the C units get for the environment, the assembler
186+
# units should get too, minus anything meaningless to the assembler" --
187+
# clang accepts the flag for `.S` input (measured, does not error), so
188+
# nothing here justifies dropping it just because assembly has no use for it.
189+
asm_args = list(args_for("probe.S"))
190+
if not asm_args:
191+
print("FAIL: could not find the .S unit's compile command at all")
192+
sys.exit(1)
193+
asm_joined = joined(iter(asm_args))
194+
missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar")
195+
if tok not in asm_joined]
196+
if missing:
197+
print(f"FAIL: the assembly unit is missing realised tokens {missing} "
198+
f"-- the [c-abi] substitution must reach .S the same as .c/.cpp\n"
199+
f" args: {asm_joined}")
200+
sys.exit(1)
201+
present = [tok for tok in ("-U__CYGWIN__", "-U__CYGWIN32__") if tok in asm_joined]
202+
if present:
203+
print(f"FAIL: __CYGWIN__/__CYGWIN32__ must stay defined on assembly too, "
204+
f"but found {present}\n args: {asm_joined}")
205+
sys.exit(1)
206+
158207
# openkalwin declared c-environment = "platform" and must NOT see the
159208
# substituted triple: it needs the real Windows identity to include platform
160209
# declarations.

tests/unit/test_modgraph.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,58 @@ TEST(Scanner, UsesResolvedPackagePrivateBuildIncludeDirs) {
541541
std::filesystem::remove_all(dir);
542542
}
543543

544+
// The realised [c-abi] environment (design 2026-09-18) broadcasts into
545+
// `privateBuild.asmflags`, and this proves the wiring on the ONE point that
546+
// went wrong first (coordinator report, openkal-musl spike): the broadcast
547+
// must reach a GAS (.S) unit's `packageAsmflags`, exactly as it reaches a
548+
// `.c`/`.cpp` unit's cflags/cxxflags -- and it must NOT reach a NASM (.asm)
549+
// unit's, because NASM has no `--target=` concept and does not recognize a
550+
// single clang-family flag. A `--target=x86_64-pc-cygwin` handed to `nasm`
551+
// is not a degraded build, it is a build that does not assemble at all, so
552+
// this is a refusal-shaped guarantee even though nothing here calls
553+
// `refusal::record`: the wrong list for the wrong assembler.
554+
TEST(Scanner, ResolvedCEnvAsmflagsReachGasUnitsOnlyNotNasm) {
555+
auto dir = make_tempdir("mcpp-scanner-cenv-asm");
556+
write(dir / "src" / "gas.S",
557+
".text\n.globl gas_marker\ngas_marker:\n ret\n");
558+
write(dir / "src" / "nasm.asm",
559+
"section .text\nglobal nasm_marker\nnasm_marker:\n ret\n");
560+
561+
mcpp::manifest::Manifest m;
562+
m.package.name = "pkg";
563+
m.modules.sources = {"src/*.S", "src/*.asm"};
564+
565+
PackageRoot p{dir, m};
566+
p.usageResolved = true;
567+
p.privateBuild.asmflags = {"--target=x86_64-pc-cygwin", "-fno-short-wchar"};
568+
569+
auto r = scan_packages({p});
570+
ASSERT_TRUE(r.errors.empty());
571+
ASSERT_EQ(r.graph.units.size(), 2u);
572+
573+
bool sawGas = false, sawNasm = false;
574+
for (auto const& u : r.graph.units) {
575+
if (u.kind == mcpp::SourceKind::GasAsm) {
576+
sawGas = true;
577+
EXPECT_NE(std::find(u.packageAsmflags.begin(), u.packageAsmflags.end(),
578+
"--target=x86_64-pc-cygwin"),
579+
u.packageAsmflags.end())
580+
<< "the realised environment must reach a GAS unit";
581+
} else if (u.kind == mcpp::SourceKind::NasmAsm) {
582+
sawNasm = true;
583+
EXPECT_EQ(std::find(u.packageAsmflags.begin(), u.packageAsmflags.end(),
584+
"--target=x86_64-pc-cygwin"),
585+
u.packageAsmflags.end())
586+
<< "NASM does not understand a clang --target= flag and must "
587+
"never receive it";
588+
}
589+
}
590+
EXPECT_TRUE(sawGas);
591+
EXPECT_TRUE(sawNasm);
592+
593+
std::filesystem::remove_all(dir);
594+
}
595+
544596
TEST(Scanner, PartitionImportFromPrimaryInterface) {
545597
// Primary module interface: `export module foo;` → logicalName = "foo".
546598
// `import :tls;` resolves to "foo:tls".

0 commit comments

Comments
 (0)