How mcpp provides a typed module API to build.mcpp so it can be written
modules-first (import mcpp;, no #include, no import std;) instead of printing
raw mcpp: protocol strings. Evaluated on five axes: 简洁 (simplicity) / 覆盖
(coverage) / 优化 (optimization) / 稳定 (stability) / 适配 (adaptability).
The helper's job is to emit the exact mcpp: wire protocol this mcpp parses.
So it is not a third-party library — it is part of the engine's ABI. Any design
that lets the helper drift from the engine's protocol version (a separately
released package, a pinned dependency) introduces skew. This single fact rules out
most of the "obvious" options and points straight at "ship it with the engine."
| Option | What | Verdict |
|---|---|---|
A. Ship a prebuilt BMI (mcpp.gcm/.pcm in the release) |
precompiled module interface | ✗ BMIs are not portable across compiler vendor/version/flags (GCC gcm is locked to the exact GCC build). Would need a combinatorial matrix of BMIs. Fragile. |
B. Header-only (#include <mcpp_build.h>) |
a shipped header | ✗ contradicts modules-first ("no headers"). (Most portable, but off-brand; kept as a mental fallback only.) |
C. Cargo model — helper is a normal [build-dependencies] package in the index |
build.mcpp depends on a published mcpp package, resolved + compiled like any dep |
△ composable, but adds a resolution step for a leaf script and reintroduces version skew (the package version vs the engine's protocol). Cargo's build-rs crate works this way — but Cargo's protocol is far more stable than a young tool's. |
| D. Zig model — helper is part of the tool, always present, version-matched | embed the module source in the binary; compile on demand against the host toolchain | ✓ chosen. Zig's std.Build ships with the compiler; the build API and the engine are one artifact, so they can never disagree. |
Source is the only toolchain-portable form. A BMI is compiler-version-locked;
source compiles against whatever host toolchain resolved for this build (gcc on
Linux, clang on macOS/Windows), at whatever version, with the same sysroot flags
the build already computes. So one embedded constexpr std::string_view adapts to
every toolchain — no matrix, no skew. This is the crux of 适配 + 稳定.
mcpp binary
└── constexpr std::string_view kMcppModuleSource // the `mcpp` module, embedded
│ (module; #include <cstdio> export module mcpp; … inline emitters)
▼ only when build.mcpp contains `import mcpp`
<proj>/target/.build-mcpp/
├── mcpp.cppm written from the embedded source
├── mcpp.gcm / .pcm compiled BMI (GCC gcm.cache/ | Clang pcm)
├── mcpp.o module object (linked into build.mcpp.bin)
└── build.mcpp.bin
- Embedded, version-matched (
build_program.cppmkMcppModuleSource). The functions mirror the directive set 1:1 andstd::printfthemcpp:lines. I/O is C-level (global module fragment#include <cstdio>), so the module needs noimport std;— neither does abuild.mcppthat onlyimport mcpp;. - Compiled on demand, into
target/— not in the project tree. GCC:-fmodules→gcm.cache/mcpp.gcm+mcpp.o; Clang:--precompile→mcpp.pcmthen-c→mcpp.o. Reuses the build's ownhost_base_flags(sysroot etc.). - Gated on actual use — mcpp scans
build.mcppforimport mcpp; only then is the module built + linked and the compile run fromtarget/.build-mcpp/(so GCC findsgcm.cache/relative to cwd, via the 0.0.79capture_execcwd). A#include-basedbuild.mcppcompiles byte-identically to before — zero blast radius.
- 简洁 — one embedded string + one compile helper; no packaging, no install, no
registry entry, no version field. The user writes
import mcpp;and it's there. - 覆盖 — GCC (gcm) on Linux + Clang (pcm) on macOS/Windows = mcpp's whole toolchain matrix (mcpp uses clang, not MSVC, on Windows). The directive API covers every wire directive 1:1.
- 优化 — built only when
build.mcppuses it AND is being (re)compiled (already gated by the declared-input cache), so a stable build.mcpp pays nothing. Cost when it does run: one ~0.3 s module compile. Future: a global per-toolchain BMI cache (~/.mcpp/bmi/build-module/<toolchain-hash>/, symlinked into each project'sgcm.cache/) would compile once per machine instead of once per project — deferred; the per-project compile is cheap and keeps the code simple. - 稳定 — embedded source ⇒ no version skew (the headline win); use-gating
⇒ existing
#includeprograms are untouched; failures surface as a clear "mcpp module compile failed" with the compiler output. - 适配 — source-on-demand adapts to any host toolchain/version automatically;
adding a directive = adding one
inlinefunction to the embedded string; per-compiler module ABI handled by the GCC/Clang branch.
import mcpp; (top-level) for brevity — build.mcpp context makes the scope
unambiguous. Future non-build helpers can live under mcpp.<sub> modules without
colliding. (import mcpp.build; was considered for namespace precision; rejected
for the common case's verbosity — revisit only if a second mcpp module appears.)
import mcpp;
int main() {
mcpp::cxxflag("-DHAVE_X=1");
mcpp::cflag("-DFOR_C");
mcpp::link_lib("m"); // -lm
mcpp::link_search("vendor/lib"); // -L…
mcpp::define("HAVE_FEATURE"); // cfg= → -DHAVE_FEATURE
mcpp::generated("src/gen.cpp");
mcpp::rerun_if_changed("config.h");
mcpp::rerun_if_env_changed("USE_FAST");
}The raw stdout protocol stays the documented low-level substrate; import mcpp; is
the typed layer over it (the Cargo build-rs-over-cargo:: shape, but
engine-bundled à la Zig).
The embedded source contains the line export module mcpp;. mcpp's default
line-based regex module scanner (used on the Windows self-host build; the P1689
compiler-driven scanner ignores string literals) read that line inside the raw
string literal as build_program.cppm declaring a second module → "file already
exports module … cannot export 'mcpp'". Fix: write the declaration with a
@MODULE@ placeholder substituted to export module at file-write time, so no
literal export module <name> text appears in mcpp's own source. (A broader fix
would be to teach the regex scanner to skip string/raw-string literals.)
- Windows/macOS Clang path is exercised by the mcpp-index
build-mcppworkspace member (itsmcpp test --workspaceruns on macOS/Windows with clang); the e2e92_build_mcpp_import.shcovers the GCC path (itrequires: gcc). - Cross
--targetbuilds still skipbuild.mcppentirely (host-only), so the module is host-only too.