// mcpp.plugins: the collection's shared library.
//
// Every member of this package is a module interface unit under rules/ or
// tools/, compiled as a host module of its own when the consumer's feature
// request names it. This unit is the lib root: it is compiled before every
// member, so a member may import it.
//
// IT HOLDS WHAT MORE THAN ONE MEMBER NEEDS, AND ONLY THAT.
//
// The lib root is the only unit every member can import. A second unit beside
// it in `[build] sources` is NOT compiled as a host module ahead of the
// members. Measured: a member importing a second lib-root unit failed with
//
// mcpp.plugins.surface: error: failed to read compiled module
// note: imports must be built before being imported
//
// because only the lib root is built first. So shared code lives here rather
// than in a file of its own, and a member that needs it writes
// `import mcpp.plugins;`.
module;
#include
export module mcpp.plugins;
import std;
// NO `import mcpp;`. That module exists only inside a build program, and this
// unit has to compile into an ordinary binary too: `mcpp.tools.embed`'s
// executable form is what lets a generated `.S` be an ACTION's output, which
// is the only way the payload it embeds can be a declared input of the edge
// that assembles it. Measured before this: an ordinary build of this package
// failed with `mcpp: failed to read compiled module`.
//
// Everything this unit used to read from that module -- the target OS, whether
// the toolchain has a GAS assembler, the package's name -- is now a parameter.
// The members under rules/ still import it; they are feature-gated, so the
// tool's own build never compiles them.
export namespace mcpp::plugins {
// THIS MUST EQUAL `[package] version` IN mcpp.toml, AND CI CHECKS THAT IT DOES.
//
// It was `0.1.1` while the package was `0.2.6`, which nothing noticed because
// nothing read it. A value that is recorded and never read cannot be wrong in a
// way anyone sees, so the fix is not only to correct it: every rule now states
// it with `mcpp::fact`, which makes a build log answer "which collection
// produced these actions" and makes a stale constant a visible defect rather
// than a dormant one.
//
// One package, one version: the number lives in mcpp.toml, and the CI step
// `the collection states its own version` compares the two.
inline constexpr std::string_view version = "0.15.2";
} // namespace mcpp::plugins
// mcpp::plugins::stage -- what the engine wrote beside a staged tree.
//
// SHARED BECAUSE TWO DIST MEMBERS READ IT. `mcpp pack` writes
// `.stage-manifest` (mcpp's docs/50, "The stage manifest"): a
// header, one `needs` line per library name the closure read (mcpp
// 2026.9.14.2+), and the list of staged files. `dist-apk` reads it to trust the
// native libraries under `lib/`, and `dist-apple` reads it to tell the dylibs
// staged beside a program from the resources staged there. Only the header and
// the `needs` lines are parsed; the file list is not.
export namespace mcpp::plugins::stage {
struct need {
std::string name; // as the needing object spells it
std::string where; // a staged path relative to the tree, `platform` or `unresolved`
};
struct manifest {
bool found = false; // the file exists beside the tree
bool walked = true; // `closure = walked`
std::string reason; // `reason = ...`, with `closure = not-walked`
std::vector needs;
};
inline manifest read_manifest(std::string tree) {
manifest m;
while (tree.size() > 1 && (tree.back() == '/' || tree.back() == '\\'))
tree.pop_back();
std::ifstream in(tree + ".stage-manifest", std::ios::binary);
if (!in) return m;
m.found = true;
std::string line;
while (std::getline(in, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line == "closure = not-walked") { m.walked = false; continue; }
if (line.starts_with("reason = ")) { m.reason = line.substr(9); continue; }
if (!line.starts_with("needs\t")) continue;
const auto second = line.find('\t', 6);
if (second == std::string::npos) continue;
m.needs.push_back({ line.substr(6, second - 6), line.substr(second + 1) });
}
return m;
}
} // namespace mcpp::plugins::stage
// mcpp::plugins::xml -- the XML two dist members read and write.
//
// SHARED BECAUSE TWO DIST MEMBERS READ IT. `dist-apk` merges library manifests
// into an application's, and `dist-apple` adds a project's Info.plist entries
// and reads the plist a provisioning profile carries. Neither needs more than
// elements, attributes and text, and neither interprets an entity, so this is
// that much and no more: no namespaces resolved, no DTD read, no validation.
export namespace mcpp::plugins::xml {
// A tree of elements and text. Comments, the XML declaration and a DOCTYPE are
// read and dropped: the documents this member writes from a tree are generated
// files, and a merged manifest carries no author to read a comment. Attribute
// values are kept exactly as written, entities included, because nothing here
// interprets them beyond comparing and substituting `${applicationId}`.
struct node {
std::string name; // empty: a text node
std::vector<:pair std::string>> attrs;
std::vector children;
std::string text; // a text node's characters
};
struct reader {
std::string_view s;
std::size_t i = 0;
std::string error;
static bool space(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
bool starts(std::string_view t) const { return s.substr(i, t.size()) == t; }
void skip_space() { while (i < s.size() && space(s[i])) ++i; }
bool skip_past(std::string_view end) {
const auto p = s.find(end, i);
if (p == std::string_view::npos) { i = s.size(); return false; }
i = p + end.size();
return true;
}
std::string name() {
const std::size_t b = i;
while (i < s.size() && !space(s[i]) && s[i] != '>' && s[i] != '/' && s[i] != '=') ++i;
return std::string(s.substr(b, i - b));
}
bool element(node& out) {
++i; // '<'
out.name = name();
if (out.name.empty()) { error = "an element without a name"; return false; }
for (;;) {
skip_space();
if (i >= s.size()) { error = "<" + out.name + "> is not terminated"; return false; }
if (starts("/>")) { i += 2; return true; }
if (s[i] == '>') { ++i; break; }
std::string attr = name();
skip_space();
if (attr.empty() || i >= s.size() || s[i] != '=') {
error = "an attribute of <" + out.name + "> has no value";
return false;
}
++i;
skip_space();
if (i >= s.size() || (s[i] != '"' && s[i] != '\'')) {
error = "attribute " + attr + " of <" + out.name + "> is not quoted";
return false;
}
const char quote = s[i++];
const auto end = s.find(quote, i);
if (end == std::string_view::npos) { error = "attribute " + attr + " is not terminated"; return false; }
out.attrs.emplace_back(std::move(attr), std::string(s.substr(i, end - i)));
i = end + 1;
}
for (;;) {
if (i >= s.size()) { error = "<" + out.name + "> is not closed"; return false; }
if (starts("")) {
i += 2;
const std::string closing = name();
skip_space();
if (closing != out.name || i >= s.size() || s[i] != '>') {
error = "" + closing + "> closes <" + out.name + ">";
return false;
}
++i;
return true;
}
if (starts("")) { error = "a comment is not terminated"; return false; } continue; }
if (starts("")) { if (!skip_past("?>")) { error = "a processing instruction is not terminated"; return false; } continue; }
if (starts("<![CDATA[")) {
const std::size_t b = i;
if (!skip_past("]]>")) { error = "a CDATA section is not terminated"; return false; }
node t;
t.text = std::string(s.substr(b, i - b));
out.children.push_back(std::move(t));
continue;
}
if (s[i] == '<') {
node child;
if (!element(child)) return false;
out.children.push_back(std::move(child));
continue;
}
const std::size_t b = i;
auto end = s.find('<', i);
if (end == std::string_view::npos) end = s.size();
i = end;
bool blank = true;
for (std::size_t j = b; j < end; ++j) if (!space(s[j])) { blank = false; break; }
if (!blank) {
node t;
t.text = std::string(s.substr(b, end - b));
out.children.push_back(std::move(t));
}
}
}
bool document(node& root) {
for (;;) {
skip_space();
if (starts("")) { skip_past("?>"); continue; }
if (starts(""); continue; }
if (starts(""); continue; }
break;
}
if (i >= s.size() || s[i] != '<') { error = "no root element"; return false; }
if (!element(root)) return false;
return true;
}
};
inline bool parse(std::string_view text, node& root, std::string& error) {
reader r{text};
if (!r.document(root)) { error = r.error; return false; }
return true;
}
inline std::string trim_copy(const std::string& s) {
std::size_t b = 0, e = s.size();
while (b < e && reader::space(s[b])) ++b;
while (e > b && reader::space(s[e - 1])) --e;
return s.substr(b, e - b);
}
inline void write(const node& n, std::string& out, int depth) {
const std::string pad(static_cast<:size_t>(depth) * 4, ' ');
if (n.name.empty()) { out += pad + trim_copy(n.text) + "\n"; return; }
out += pad + "<" + n.name;
for (auto const& a : n.attrs) out += " " + a.first + "=\"" + a.second + "\"";
if (n.children.empty()) { out += "/>\n"; return; }
if (n.children.size() == 1 && n.children.front().name.empty()) {
out += ">" + n.children.front().text + "" + n.name + ">\n";
return;
}
out += ">\n";
for (auto const& c : n.children) write(c, out, depth + 1);
out += pad + "" + n.name + ">\n";
}
inline std::string attr_of(const node& n, std::string_view key) {
for (auto const& a : n.attrs) if (a.first == key) return a.second;
return {};
}
inline void set_attr(node& n, const std::string& key, const std::string& value) {
for (auto& a : n.attrs) if (a.first == key) { a.second = value; return; }
n.attrs.emplace_back(key, value);
}
} // namespace mcpp::plugins::xml
// mcpp::plugins::json -- the JSON two dist members read.
//
// SHARED BECAUSE TWO DOCUMENTS ARE READ WITH IT. `dist-apk` reads coursier's
// resolution report, and `mcpp::plugins::graph` below reads the engine's graph
// document for `dist-apk` and `dist-apple`. A second parser for the second
// document is how two readers come to disagree about one file, so there is one.
// It reads objects, arrays, strings (with `\u` escapes, surrogate pairs
// included, and a refusal for a surrogate that is not part of one), numbers,
// `true`, `false` and `null`; a number keeps its spelling in `text`.
export namespace mcpp::plugins::json {
struct value {
enum class kind { null, boolean, number, string, array, object };
kind type = kind::null;
std::string text; // string, number or boolean spelling
std::vector items;
std::vector<:pair value>> members;
const value* get(std::string_view key) const {
for (auto const& m : members) if (m.first == key) return &m.second;
return nullptr;
}
};
struct reader {
std::string_view s;
std::size_t i = 0;
void skip_space() {
while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
}
// The four hexadecimal digits of one `\u` escape, `i` already past the `u`.
bool hex4(unsigned& cp) {
if (i + 4 > s.size()) return false;
cp = 0;
for (int k = 0; k < 4; ++k) {
const char h = s[i++];
cp <<= 4;
if (h >= '0' && h <= '9') cp |= static_cast(h - '0');
else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10);
else return false;
}
return true;
}
bool string(std::string& out) {
if (i >= s.size() || s[i] != '"') return false;
++i;
while (i < s.size() && s[i] != '"') {
if (s[i] != '\\') { out += s[i++]; continue; }
if (++i >= s.size()) return false;
const char e = s[i++];
switch (e) {
case 'n': out += '\n'; break;
case 't': out += '\t'; break;
case 'r': out += '\r'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'u': {
// A code point above U+FFFF reaches JSON as a surrogate
// PAIR, which is two `\u` escapes. Encoding each half on its
// own produces two three-byte sequences holding unpaired
// surrogates -- not UTF-8, and accepted by nothing that
// reads the manifest this value is written into. The pair is
// combined here, and a surrogate that is not part of one is
// refused, so a malformed document is a refusal rather than
// a silently corrupted string.
unsigned cp = 0;
if (!hex4(cp)) return false;
if (cp >= 0xd800 && cp <= 0xdbff) {
if (i + 6 > s.size() || s[i] != '\\' || s[i + 1] != 'u') return false;
i += 2;
unsigned lo = 0;
if (!hex4(lo)) return false;
if (lo < 0xdc00 || lo > 0xdfff) return false;
cp = 0x10000 + ((cp - 0xd800) << 10) + (lo - 0xdc00);
} else if (cp >= 0xdc00 && cp <= 0xdfff) {
return false; // a low surrogate with no high half
}
if (cp < 0x80) out += static_cast(cp);
else if (cp < 0x800) { out += static_cast(0xc0 | (cp >> 6)); out += static_cast(0x80 | (cp & 0x3f)); }
else if (cp < 0x10000) { out += static_cast(0xe0 | (cp >> 12)); out += static_cast(0x80 | ((cp >> 6) & 0x3f)); out += static_cast(0x80 | (cp & 0x3f)); }
else {
out += static_cast(0xf0 | (cp >> 18));
out += static_cast(0x80 | ((cp >> 12) & 0x3f));
out += static_cast(0x80 | ((cp >> 6) & 0x3f));
out += static_cast(0x80 | (cp & 0x3f));
}
break;
}
default: out += e;
}
}
if (i >= s.size()) return false;
++i;
return true;
}
bool parse_value(value& out) {
skip_space();
if (i >= s.size()) return false;
const char c = s[i];
if (c == '"') { out.type = value::kind::string; return string(out.text); }
if (c == '{') {
out.type = value::kind::object;
++i;
skip_space();
if (i < s.size() && s[i] == '}') { ++i; return true; }
for (;;) {
skip_space();
std::string key;
if (!string(key)) return false;
skip_space();
if (i >= s.size() || s[i] != ':') return false;
++i;
value v;
if (!parse_value(v)) return false;
out.members.emplace_back(std::move(key), std::move(v));
skip_space();
if (i < s.size() && s[i] == ',') { ++i; continue; }
if (i < s.size() && s[i] == '}') { ++i; return true; }
return false;
}
}
if (c == '[') {
out.type = value::kind::array;
++i;
skip_space();
if (i < s.size() && s[i] == ']') { ++i; return true; }
for (;;) {
value v;
if (!parse_value(v)) return false;
out.items.push_back(std::move(v));
skip_space();
if (i < s.size() && s[i] == ',') { ++i; continue; }
if (i < s.size() && s[i] == ']') { ++i; return true; }
return false;
}
}
const std::size_t b = i;
while (i < s.size() && s[i] != ',' && s[i] != '}' && s[i] != ']' &&
s[i] != ' ' && s[i] != '\n' && s[i] != '\r' && s[i] != '\t') ++i;
out.text = std::string(s.substr(b, i - b));
if (out.text == "null") out.type = value::kind::null;
else if (out.text == "true" || out.text == "false") out.type = value::kind::boolean;
else out.type = value::kind::number;
return !out.text.empty();
}
};
// The whole text is one value, with nothing but white space after it.
inline bool parse_json(std::string_view text, value& out) {
reader r{text};
if (!r.parse_value(out)) return false;
r.skip_space();
return r.i == r.s.size();
}
// A string member of an object, or empty when absent or not a string.
inline std::string string_of(const value* v) {
return v && v->type == value::kind::string ? v->text : std::string();
}
} // namespace mcpp::plugins::json
// mcpp::plugins::graph -- the resolved dependency graph the engine states.
//
// mcpp 2026.9.16.1 gives the ROOT project's build program `MCPP_GRAPH_FILE`
// (`mcpp::graph_file()`), a document naming every package of the resolved graph,
// dependencies before their requesters, each with its manifest directory and its
// `[package.metadata]` verbatim (mcpp docs/30, "Reading the resolved graph"). A
// member that merges what libraries contribute reads a table of its own out of
// that metadata: `[package.metadata.dist-apk]`, `[package.metadata.dist-apple]`.
//
// READ THROUGH THE ENVIRONMENT, NOT `mcpp::graph_file()`. This unit does not
// import `mcpp` (see the top of this file), and a member that called the
// accessor would not compile against an engine older than the one that added it.
// An unset or empty variable is an older engine, or a dependency's program, and
// reads as a graph with no packages, which leaves every member's behaviour as it
// was.
export namespace mcpp::plugins::graph {
struct package {
std::string canonical; // `namespace.name@version`
std::string name;
std::string namespace_;
std::string version;
bool root = false;
std::string manifest_dir; // absolute
json::value metadata; // the package's [package.metadata], an object
};
struct document {
bool present = false; // MCPP_GRAPH_FILE named a file
std::string path;
std::vector packages; // dependencies before requesters
};
// The graph this build program was given, or the reason it cannot be read. A
// variable naming a file that is missing or not a `mcpp.graph` document is an
// error rather than an empty graph: the engine promised a document, and reading
// nothing would silently drop every contribution.
inline std::expected read() {
document d;
const char* env = std::getenv("MCPP_GRAPH_FILE");
if (!env || !*env) return d;
d.present = true;
d.path = env;
std::ifstream in(d.path, std::ios::binary);
if (!in)
return std::unexpected(std::format("the graph document {} cannot be read", d.path));
const std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator());
json::value doc;
if (!json::parse_json(text, doc) || doc.type != json::value::kind::object)
return std::unexpected(std::format("the graph document {} is not JSON", d.path));
if (json::string_of(doc.get("kind")) != "mcpp.graph")
return std::unexpected(std::format("the graph document {} is not a mcpp.graph document", d.path));
const json::value* list = doc.get("packages");
if (!list || list->type != json::value::kind::array)
return std::unexpected(std::format("the graph document {} has no packages array", d.path));
for (auto const& entry : list->items) {
if (entry.type != json::value::kind::object) continue;
package p;
if (const json::value* id = entry.get("package")) {
p.canonical = json::string_of(id->get("canonical"));
p.name = json::string_of(id->get("name"));
p.namespace_ = json::string_of(id->get("namespace"));
p.version = json::string_of(id->get("version"));
}
if (const json::value* r = entry.get("root"))
p.root = r->type == json::value::kind::boolean && r->text == "true";
p.manifest_dir = json::string_of(entry.get("manifest_dir"));
if (const json::value* m = entry.get("metadata"); m && m->type == json::value::kind::object)
p.metadata = *m;
d.packages.push_back(std::move(p));
}
return d;
}
// The package's `[package.metadata.]` table, or null.
inline const json::value* table_of(const package& p, std::string_view tool) {
const json::value* t = p.metadata.get(tool);
return t && t->type == json::value::kind::object ? t : nullptr;
}
// A path a package states, made absolute against its manifest directory.
inline std::string resolve(const package& p, const std::string& path) {
if (path.empty() || std::filesystem::path(path).is_absolute()) return path;
return (std::filesystem::path(p.manifest_dir) / path).lexically_normal().string();
}
// How a diagnostic names a package: its canonical identity, or its name.
inline std::string label_of(const package& p) {
return !p.canonical.empty() ? p.canonical : p.name;
}
} // namespace mcpp::plugins::graph
// mcpp::plugins::names -- the derivations that turn a path into a C++ name.
//
// THESE ARE SHARED BECAUSE THEY WERE COPIED. `common_base_dir` and
// `namespace_of` were written in `rules/spirv.cppm` and written again in
// `rules/slang.cppm`, and `mcpp.tools.island` is the third caller needing the
// same answers. Two copies that agree today are still two copies: a directory
// named `default` or `2d` has to get ONE answer, and one function is how that
// is guaranteed rather than two files that happen to say the same thing.
export namespace mcpp::plugins::names {
// `a` made relative to the directory `b`, as strings: separators unified, and
// the prefix stripped when `a` lies under `b`; `a` unchanged otherwise.
//
// NOTHING IN THIS PACKAGE INSTANTIATES `std::filesystem::path`'s ITERATOR --
// see `components()` below for the compiler that refuses it. The members'
// relative-path arithmetic is this string function, defined once here.
inline std::string relative_to(std::string a, std::string b) {
for (auto& c : a) if (c == '\\') c = '/';
for (auto& c : b) if (c == '\\') c = '/';
while (!b.empty() && b.back() == '/') b.pop_back();
if (!b.empty() && a.starts_with(b + "/")) return a.substr(b.size() + 1);
return a;
}
// A GENERATED NAME THE C++ COMPILER WILL ACCEPT.
//
// Three transformations, and the third is the one every hand-rolled copy of
// this function was missing. Non-identifier characters become `_`; a leading
// digit gets a `_` in front; and a result that is a KEYWORD gets a trailing `_`.
//
// The keyword case is not hypothetical. The first two rules accept `default`,
// `template`, `operator`, `private` and `union` unchanged -- they are valid
// identifiers to a character filter and reserved to the compiler -- and
// `shaders/default/` is an ordinary name for a shader directory. What it
// produced was `namespace default {` in a generated file, and an error naming a
// line its author never wrote.
//
// TRAILING `_`, not a prefix: `_default` is reserved at namespace scope
// (a leading underscore in the global namespace), and prefixing would trade one
// reserved name for another.
//
// The list is the keywords of the standard this collection targets. A word that
// is contextual rather than reserved (`final`, `override`, `import`, `module`)
// is a legal identifier and is left alone.
inline bool is_cxx_keyword(std::string_view w) {
static constexpr std::string_view kWords[] = {
"alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", "bitor",
"bool", "break", "case", "catch", "char", "char8_t", "char16_t",
"char32_t", "class", "compl", "concept", "const", "consteval",
"constexpr", "constinit", "const_cast", "continue", "co_await",
"co_return", "co_yield", "decltype", "default", "delete", "do", "double",
"dynamic_cast", "else", "enum", "explicit", "export", "extern", "false",
"float", "for", "friend", "goto", "if", "inline", "int", "long",
"mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr",
"operator", "or", "or_eq", "private", "protected", "public", "register",
"reinterpret_cast", "requires", "return", "short", "signed", "sizeof",
"static", "static_assert", "static_cast", "struct", "switch",
"template", "this", "thread_local", "throw", "true", "try", "typedef",
"typeid", "typename", "union", "unsigned", "using", "virtual", "void",
"volatile", "wchar_t", "while", "xor", "xor_eq",
};
for (auto k : kWords) if (k == w) return true;
return false;
}
// `fallback` is used when the input sanitises to nothing, which a file named
// only in punctuation does.
//
// INDEXED RATHER THAN A RANGE-FOR over the string, for the reason recorded in
// `mcpp.tools.island`: iterating a `std::string` inside an exported inline
// function makes GCC 16 instantiate its iterator in this BMI, and a consumer's
// build program then fails to compile on `always_inline` in a header naming
// neither this file nor this loop.
inline std::string identifier(std::string_view raw, std::string_view fallback) {
std::string s;
for (std::size_t i = 0; i < raw.size(); ++i) {
const char c = raw[i];
s += (std::isalnum(static_cast(c)) || c == '_') ? c : '_';
}
if (s.empty()) s = std::string(fallback);
if (!s.empty() && std::isdigit(static_cast(s.front())))
s.insert(s.begin(), '_');
if (is_cxx_keyword(s)) s += '_';
return s;
}
// ---- internals -------------------------------------------------------------
inline std::vector<:string> split_module_name(std::string_view name) {
std::vector<:string> out;
for (std::size_t i = 0; i <= name.size();) {
auto dot = name.find('.', i);
auto one = dot == std::string_view::npos ? name.substr(i) : name.substr(i, dot - i);
if (!one.empty()) out.emplace_back(one);
if (dot == std::string_view::npos) break;
i = dot + 1;
}
return out;
}
// THE BASE DIRECTORY IS DERIVED, NOT ASKED FOR.
//
// A payload's namespace comes from where it sits relative to the tree the
// project globbed, so something has to say where that tree starts. Asking the
// project would put a second spelling of the glob in the manifest, and the two
// would disagree the first time a glob moved. The shallowest directory every
// path shares is the same answer without the second spelling: for
// `shaders/*.comp` it is `shaders` and every namespace is empty; for
// `shaders/a/x.comp` and `shaders/b/y.comp` it is still `shaders`, and the two
// land in `::a` and `::b`.
//
// A single path has no common prefix with anything, so its own directory is the
// base and its namespace is empty -- which is the same answer the general case
// gives once a second file appears beside it.
// A path's components as strings: both separators split, empty and `.`
// components dropped. This is the one place the lib root reads a path apart,
// and it does so WITHOUT `std::filesystem::path`'s iterator on purpose:
//
// A MODULE THAT INSTANTIATES `_Path_iterator` POISONS ITS IMPORTERS UNDER MSVC
// 14.52. Measured on xrgui's CI (14.52.36629 and .36725): this unit compiled
// while it iterated paths, and every member importing it that then touched
// `std::filesystem` at all failed inside the STL --
//
// include\filesystem(1572): error C2801: '..._Path_iterator<...>::operator =='
// must be a non-static member
//
// -- the iterator's hidden-friend comparison, refused when the importer meets
// it both through `import std` and through this module's BMI. Removing the
// calls from the members changed nothing; the instantiation had to leave the
// lib root. Component comparison is what `lexically_relative` bought in 0.5.2
// (a Windows separator bug), and splitting on both separators keeps that.
inline std::vector<:string> components(std::string_view path) {
std::vector<:string> out;
std::string cur;
auto flush = [&] { if (!cur.empty() && cur != ".") out.push_back(cur); cur.clear(); };
for (char c : path) { if (c == '/' || c == '\\') flush(); else cur += c; }
flush();
return out;
}
// The directory part of a path, as written: everything before the last
// separator, or empty when there is none.
inline std::string_view parent_of(std::string_view path) {
const auto slash = path.find_last_of("/\\");
return slash == std::string_view::npos ? std::string_view{} : path.substr(0, slash);
}
inline std::string common_base_dir(std::span paths) {
std::vector<:string> prefix;
bool first = true;
for (auto const& src : paths) {
auto segs = components(parent_of(src));
if (first) { prefix = std::move(segs); first = false; continue; }
std::size_t keep = 0;
while (keep < prefix.size() && keep < segs.size() && prefix[keep] == segs[keep]) ++keep;
prefix.resize(keep);
}
std::string out;
for (auto const& s : prefix) { if (!out.empty()) out += '/'; out += s; }
return out;
}
// The namespace segments a file sits in, below the group's own: the path from
// the base directory to the file, sanitised one segment at a time.
//
// RELATIVE BY PATH ARITHMETIC, NOT BY STRING SURGERY, AND THAT IS A WINDOWS
// FIX. This trimmed `base` off the front of the directory as a STRING and
// iterated what was left. On Windows the two spellings differ even when the
// two paths are the same: a caller states a root with forward slashes, and
// `directory_iterator` appends with the preferred separator, so the leftover
// was `\image` rather than `image`. Iterating that yields the ROOT DIRECTORY
// as its first component, which sanitised to `_` -- and the payload landed in
// `myapp::shaders::_::image`, a namespace no consumer writes. Measured: the
// island fixture failed to compile on windows-2022 with `no member named
// 'image' in namespace 'island_interface::kernels'`, while the same fixture
// passed on Linux and macOS.
//
// COMPONENTS are compared, so the separator a caller happened to write is not
// part of the question -- `components()` splits on both. A base that is not a
// prefix is a caller error rather than a namespace; it answers with no
// segments rather than with the whole absolute path, which is what the first
// string form produced. (Through `components()` rather than
// `lexically_relative` for the reason stated on it.)
inline std::vector<:string> namespace_of(std::string_view src, std::string_view base) {
std::vector<:string> out;
auto dir = components(parent_of(src));
if (!base.empty()) {
const auto b = components(base);
if (b.size() > dir.size()) return out;
for (std::size_t i = 0; i < b.size(); ++i) if (b[i] != dir[i]) return out;
dir.erase(dir.begin(), dir.begin() + static_cast<:ptrdiff_t>(b.size()));
}
for (auto const& s : dir) {
if (s == "..") continue;
// `shaders/default/` is an ordinary directory name and
// `namespace default {` is not a namespace.
out.push_back(identifier(s, "dir"));
}
return out;
}
} // namespace mcpp::plugins::names
// mcpp.plugins.surface -- the interface a consumer names for an embedded payload.
//
// WHY THIS IS SHARED RATHER THAN PER-RULE.
//
// `mcpp.rules.spirv` and `mcpp.rules.slang` produce the same kind of thing: a
// block of bytes the program hands to a device API. `mcpp.tools.embed` produces
// it from a file that was already there. The three differ in who produces the
// bytes and disagree about nothing else, so the declaration a consumer reads is
// written once, here, and the shape is identical whichever produced it. A rule
// that wrote its own would be a second copy of a decision, and the two would
// drift the way the two shader compilers' headers drifted before 0.2.6.
//
// THE INTERFACE IS A FUNCTION, NOT A VARIABLE.
//
// A variable cannot keep one shape across the ways bytes can be stored, because
// `constexpr` and `extern` are mutually exclusive: an array compiled into a
// translation unit can be constant-evaluated and one living in a section cannot.
// A function can, so where the bytes live stays an option a project revises
// without touching a consumer.
//
// THE INTERFACE NAMES NO STANDARD-LIBRARY TYPE, AND THAT IS MEASURED.
//
// The same 1 MB payload, reached four ways, compiled with GCC 16.1:
//
// data in the module (`export inline constexpr`) BMI 6 282 240 B
// data in a header source 3 145 728 B
// data in an object, interface returns std::span BMI 1 313 968 B
// data in an object, interface returns a std-free POD BMI 1 808 B
//
// The third row is 727 times the fourth, and its cost is FIXED rather than
// proportional to the payload: the 1.28 MB is ``'s templates, present
// whether the payload is 16 KB or 16 MB. A consumer that wants a `std::span`
// constructs one from the two members, and `` is then included by the
// consumer that uses it rather than by every consumer that imports this.
//
// There is a second, independent reason for the same decision. `import std;`
// requires `std.gcm` to have been built, which a project with
// `modules = true, import_std = false` has not done. A std-free interface needs
// neither.
//
// WHY THE ACCESSORS HAVE C LANGUAGE LINKAGE.
//
// A function declared in a module interface has module linkage and can only be
// defined by a unit attached to that module. Defining them would therefore need
// a module implementation unit, and the definitions include the generated data
// headers -- which would put the arrays back into the interface's own
// compilation. Declaring the accessors `extern "C"` instead gives them external
// linkage, so an ordinary translation unit defines them, the module interface
// holds two declarations and one inline call per payload, and the bytes are
// never seen by the interface at all.
//
// It also removes a portability question: module implementation units are the
// least exercised corner of every implementation this package supports, and
// nothing here needs them.
//
// ONE COPY OF THE BYTES.
//
// A generated data header declares `static const uint32_t []`, so every
// translation unit that includes it gets its own copy. Under this surface
// exactly one translation unit includes it -- the generated implementation --
// and every consumer reaches the same array through the accessor.
export namespace mcpp::plugins::surface {
// The name derivations live in `mcpp::plugins::names`. They are reachable under
// this namespace as well, because every rule in this collection already spells
// them this way -- one definition under two spellings is not two definitions.
using names::identifier;
using names::split_module_name;
using names::common_base_dir;
using names::namespace_of;
// How a consumer names the payloads.
//
// `module_` is the default where the project builds C++ modules, and
// `c_header` is what a project without them gets. The choice does not change
// any declaration's shape: the same struct, the same function names, the same
// namespaces. Only the file a consumer reaches them through differs.
enum class kind { module_, c_header };
// WHICH HALF OF THE SURFACE A CALL WRITES.
//
// The two are separate because their INPUTS are. The interface is a function of
// the item list: names, namespaces, the accessor declarations. The body is a
// function of the item list AND, under `storage::object`, of the payloads
// themselves -- the assembly names each one in `.incbin`, and the object it
// produces is those bytes.
//
// Declaring them as one action would make a payload change rewrite the
// interface too, and rebuild every BMI that imports it. Declaring them
// separately states what is true and costs a consumer nothing.
enum class half { interface_, body };
// The element the accessor hands back. `word32` is what an API taking
// `const uint32_t*` wants -- `vkCreateShaderModule` is the case this package
// has -- and asking for it here is cheaper than a reinterpret_cast at every
// call site, which is undefined behaviour on an under-aligned byte array.
enum class element { byte_, word32 };
// WHERE THE BYTES LIVE. Orthogonal to `kind`, which decides how a consumer
// NAMES them: the declarations are identical under all three, so a project
// changes this and no consumer changes.
//
// WHICH ONE TO USE IS A MEASUREMENT, NOT A PREFERENCE. With GCC 16.1 on this
// project's own fixtures, 100 payloads of 16 KB each:
//
// header route compile 0.64s + link 0.44s = 1.10s
// object route convert 1.28s + compile 0.44s + link 0.46s = 2.17s
//
// The header route is faster, because at a real shader's size neither route has
// a measurable marginal cost and the total is decided by how many processes
// start -- and one compiler invocation absorbs many headers. The crossover is
// the TOTAL embedded byte count, not the payload count: below about 1 MB the
// header route wins, and above about 4 MB the compiler's slightly superlinear
// curve loses by an order of magnitude (2.31s against 0.116s at 4 MB). Source
// expansion is a constant 2.75x, which is the second half of the same reason.
//
// So `header` is the default and `object` is what a project reaches for when it
// has more payload than that, not what it reaches for because objects sound
// tidier.
enum class storage {
// The payload is a C array in generated source, compiled into the program.
// No assembler involved, so it works on every toolchain including MSVC.
header,
// The payload is a section in an object, reached through `.incbin` in a
// generated `.S`. The bytes never pass through the C++ compiler.
//
// Requires a GAS-capable assembler. Every gcc and clang toolchain has one
// on all three platforms; MSVC does not, and mcpp refuses `.S` under it, so
// the emitter falls back to `header` there and says so once.
object,
// The payload is written beside the artifact and read at run time. The
// program's correctness then depends on its working directory, which is the
// reason this is not the default -- but it is what shader hot-reload needs,
// and what a payload too large to link needs.
sidecar,
};
// One payload in a group.
struct item {
// The C++ identifier, already sanitised by the caller: `blur_comp`.
std::string identifier;
// Namespace segments BELOW the group's own, from the payload's directory
// relative to its root: `{"post"}` for `shaders/post/tonemap.frag`. This is
// what keeps two payloads with one stem from colliding, and what makes the
// name a consumer writes say where the payload came from.
std::vector<:string> name_space;
// The generated data header this payload's bytes arrive in, AS AN
// `#include` WRITES IT -- relative to the directory the caller put on the
// include path, not an absolute path and not a bare file name. The
// generated tree mirrors the payload tree, so two payloads sharing a stem
// in different directories reach different headers, and a bare file name
// could not tell them apart.
std::string data_header;
// The array that header declares, qualified if it sits in a namespace.
// Unused under `storage::object`, where the linker symbol is derived from
// the accessor name instead, and under `storage::sidecar`.
std::string data_symbol;
// The payload file itself, as an absolute path. Required by `object`, whose
// generated `.S` names it in `.incbin`, and by `sidecar`, which copies it.
// The file need not exist yet: `.incbin` resolves its argument at assembly
// time, which is after the action that writes it has run, and that is what
// lets the whole surface be written at plan time.
std::string payload_path;
// Where a sidecar payload is found at run time, relative to the artifact.
std::string sidecar_name;
// The payload's size IN BYTES, as an expression valid where the generated
// implementation writes it. Empty means `sizeof `.
//
// IT EXISTS BECAUSE `sizeof` IS NOT ALWAYS THE ANSWER. `mcpp.tools.embed`
// can append a terminating zero byte that its own `_size` constant does not
// count, so `sizeof` and `_size` differ by one and only one of them is what
// the payload is. Measured: a group of null-terminated text payloads
// reported one byte too many and the fixture's comparison failed on a
// trailing NUL. A caller that knows the difference states it here rather
// than letting the generator guess.
std::string data_size_expr;
};
struct options {
kind surface = kind::module_;
storage store = storage::header;
element elem = element::word32;
// The module a consumer imports, e.g. `myapp.shaders`. The namespace is
// this name with `.` replaced by `::`, which is why there is no second
// field for it: two spellings of one identity drift.
std::string module_name;
// Where the generated interface and implementation are written. The caller
// adds this to the include path when the surface is `c_header`.
std::string out_dir;
// A short phrase naming what produced the payloads, for the generated
// files' first line: "mcpp.rules.spirv".
std::string produced_by;
// ââ What this generator is NOT allowed to find out for itself âââââââââââ
//
// THE TWO FIELDS BELOW ARE WHY THIS MODULE IMPORTS ONLY `std`.
//
// They were `mcpp::target_os()` and `mcpp::compiler()`, read here. That
// made the generator unusable anywhere except inside a build program --
// which is the one place it must NOT be the only usable form, because a
// payload dependency can only enter the build graph if the generation is
// an ACTION, and an action's command is a binary. A binary cannot import
// the build-program module: measured, an ordinary build of this package
// fails with "mcpp: failed to read compiled module".
//
// So they are parameters. The caller has both answers already, and a
// generator that takes its inputs rather than reading its environment is
// the same code in a build program and in a tool.
// The target's operating system, as mcpp spells it -- what decides the
// assembly dialect: the section directive and whether a symbol carries a
// leading underscore. Required under `storage::object`; ignored otherwise.
std::string target_os;
// Whether a GAS-compatible assembler exists for this toolchain. False
// under MSVC, which has none, and where mcpp refuses `.S` outright. It is
// the CALLER's answer because the caller knows the compiler; this
// generator degrades `object` to `header` when it is false and reports
// that through `emitted::store` rather than by printing.
bool has_gas_assembler = true;
};
// What the caller hands back to mcpp.
struct emitted {
// The interface unit: a `.cppm` under `module_`, a `.h` under `c_header`.
std::string interface_file;
// The generated `.S` under `storage::object`, empty otherwise. The caller
// adds it to the build the same way it adds the implementation.
std::string assembly_file;
// The storage actually used. Differs from what was asked for when the
// toolchain cannot assemble: MSVC has no GAS, so `object` degrades to
// `header` and this says so rather than leaving the caller to assume.
storage store = storage::header;
// The translation unit defining the accessors. Always a plain `.cpp`.
std::string impl_file;
// Non-empty under `module_`: what the interface unit provides, which the
// caller declares with `mcpp::action::provides` or lets the scan find.
std::string module_name;
// Non-empty under `c_header`: the directory to put on the include path.
std::string include_dir;
};
// The linker symbol an accessor carries. It is derived from the module name and
// the payload's full namespace path rather than from the identifier alone,
// because two groups in one link unit -- a project with shaders and with
// embedded data -- would otherwise define the same symbol twice and the second
// definition would be the one nobody expected.
inline std::string accessor_base(const options& opt, const item& it) {
std::string s = "mcpp_embed";
auto add = [&](std::string_view part) {
s += '_';
for (char c : part)
s += (std::isalnum(static_cast(c)) || c == '_') ? c : '_';
};
for (auto const& seg : split_module_name(opt.module_name)) add(seg);
for (auto const& seg : it.name_space) add(seg);
add(it.identifier);
return s;
}
inline const char* element_type(element e) {
return e == element::word32 ? "unsigned int" : "unsigned char";
}
// `unsigned int` is 32 bits on every target this package supports, and the
// generated implementation asserts it rather than assuming it: a target where
// it is not would otherwise hand a Vulkan driver a pointer into misread data,
// and the failure would be a device error naming nothing.
inline const char* element_width_assertion(element e) {
return e == element::word32
? "static_assert(sizeof(unsigned int) == 4,\n"
" \"mcpp.plugins.surface: element::word32 assumes a 32-bit `unsigned int`\");\n"
: "";
}
inline bool write_if_different(const std::string& path, std::string_view text) {
std::error_code ec;
std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
if (std::ifstream in(path, std::ios::binary); in) {
std::string old((std::istreambuf_iterator(in)), std::istreambuf_iterator());
if (old == text) return true;
}
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) return false;
out.write(text.data(), static_cast<:streamsize>(text.size()));
return static_cast(out);
}
// Open and close the namespaces an item sits in, below the group's own.
inline std::string open_namespaces(const std::vector<:string>& segs) {
std::string s;
for (auto const& one : segs) s += "namespace " + one + " {\n";
return s;
}
inline std::string close_namespaces(const std::vector<:string>& segs) {
std::string s;
for (auto i = segs.rbegin(); i != segs.rend(); ++i) s += "} // namespace " + *i + "\n";
return s;
}
// The declarations shared by both surfaces. What differs between a module
// interface and a header is the preamble and how the group's namespace is
// opened; the body below is byte-identical, which is what makes switching
// surfaces a change no consumer can observe.
// The accessor declarations, at GLOBAL scope.
//
// C language linkage makes a name the same entity whatever namespace declares
// it, so these would resolve from inside the group's namespace as well. They are
// written outside it because the generated implementation defines them at global
// scope, and a reader comparing the two files should not have to know that rule
// to see that they match.
inline std::string extern_c_declarations(std::span items, const options& opt) {
const char* elem = element_type(opt.elem);
std::string s = "extern \"C\" {\n";
for (auto const& it : items) {
const auto base = accessor_base(opt, it);
s += std::format("const {}* {}_data();\n", elem, base);
s += std::format("unsigned long {}_size();\n", base);
}
s += "}\n";
return s;
}
inline std::string declarations(std::span items, const options& opt) {
const char* elem = element_type(opt.elem);
std::string s;
s += std::format(
"// The payload, as the device API wants it. Free of standard-library\n"
"// types on purpose: see mcpp.plugins.surface.\n"
"struct payload {{\n"
" const {}* code; // e.g. VkShaderModuleCreateInfo::pCode\n"
" unsigned long size_bytes; // e.g. VkShaderModuleCreateInfo::codeSize\n"
"}};\n\n", elem);
// Grouped by namespace path so a directory's payloads are emitted together
// and each namespace is opened once.
std::vector<:string> openNow;
auto reopen = [&](const std::vector<:string>& want) {
if (openNow == want) return;
s += close_namespaces(openNow);
s += open_namespaces(want);
openNow = want;
};
for (auto const& it : items) {
reopen(it.name_space);
const auto base = accessor_base(opt, it);
// THROUGH `identifier`, HERE RATHER THAN IN EACH PRODUCER. This is the
// one line that turns `item::identifier` into something a compiler
// parses, and a rule that builds the field from a file stem cannot know
// it has produced `my-shader_comp` or `default` until it gets here.
s += std::format("inline payload {}() {{ return {{ {}_data(), {}_size() }}; }}\n",
identifier(it.identifier, "payload"), base, base);
}
reopen({});
return s;
}
// ONE OBJECT FORMAT PER PLATFORM, AND THE DIFFERENCES ARE NOT COSMETIC.
//
// `.incbin` is the portable part: gas and clang's integrated assembler both
// accept it everywhere, and it resolves its argument at ASSEMBLY time, which is
// why this file can be written before the payload exists. What is not portable
// is the section directive and whether a C symbol carries a leading underscore.
//
// ELF `.section .rodata`, symbol as written
// Mach-O `.section __TEXT,__const`, symbol PREFIXED with `_`
// COFF `.section .rdata,"dr"`, symbol as written on x86_64
//
// Mach-O's underscore is the one that fails quietly in the other direction: an
// assembly label without it defines a symbol the C++ side never resolves, and
// the link error names the accessor rather than the missing prefix.
struct asm_dialect {
std::string_view section;
std::string_view symbol_prefix;
};
inline asm_dialect dialect_for(std::string_view targetOs) {
if (targetOs == "macos" || targetOs == "macosx" || targetOs == "darwin")
return { ".section __TEXT,__const", "_" };
if (targetOs == "windows") return { ".section .rdata,\"dr\"", "" };
return { ".section .rodata", "" };
}
// `.balign 4` rather than nothing: `VkShaderModuleCreateInfo::pCode` requires
// four-byte alignment, and a section directive alone does not promise it. The
// header route gets alignment from the array's element type; this route has to
// ask for it.
inline std::string assembly_for(std::span items, const options& opt,
std::string_view targetOs) {
const auto d = dialect_for(targetOs);
std::string s =
"// Generated by mcpp.plugins.surface. Do not edit.\n"
"//\n"
"// The payloads, as sections rather than as C arrays. `.incbin` resolves\n"
"// its argument when this file is assembled, which is after the actions\n"
"// that write those files have run.\n";
for (auto const& it : items) {
const auto base = accessor_base(opt, it);
s += std::format("\n {}\n"
" .globl {}{}_begin\n"
" .balign 4\n"
"{}{}_begin:\n"
" .incbin \"{}\"\n"
" .globl {}{}_end\n"
"{}{}_end:\n",
d.section,
d.symbol_prefix, base,
d.symbol_prefix, base,
it.payload_path,
d.symbol_prefix, base,
d.symbol_prefix, base);
}
return s;
}
// ---- the tool ---------------------------------------------------------------
// EVERY FILE THIS SURFACE WILL PRODUCE, WITHOUT PRODUCING ANY OF THEM.
//
// A rule declares these as an action's outputs, and mcpp requires an output to
// be NAMED before the graph is built even though its content arrives later. So
// the names cannot come from having written the files -- which is the coupling
// that forced generation to happen at plan time, and with it the whole
// staleness this round removes.
//
// Pure: no items, no filesystem, no environment. The one decision it makes is
// the MSVC degradation, which is a property of the toolchain rather than of any
// payload, and `emitted::store` is where the caller reads the verdict.
inline emitted outputs(const options& opt) {
emitted out;
out.store = opt.store;
// MSVC HAS NO GAS, AND mcpp REFUSES `.S` UNDER IT. `src/build/prepare.cppm`
// says so outright: "GAS assembly sources (.S/.s) are not supported by the
// MSVC toolchain". Object storage degrades to header storage there rather
// than producing a file the build will refuse, and the SURFACE does not
// change -- the declarations are identical under both.
if (out.store == storage::object && !opt.has_gas_assembler)
out.store = storage::header;
const auto dir = std::filesystem::path(opt.out_dir);
out.interface_file = (dir / (opt.module_name
+ (opt.surface == kind::module_ ? ".cppm" : ".h"))).string();
out.impl_file = (dir / (opt.module_name + ".impl.cpp")).string();
// Named only under the storage that produces one, so a caller can test the
// string rather than having to test the storage a second time.
if (out.store == storage::object)
out.assembly_file = (dir / (opt.module_name + ".payload.S")).string();
if (opt.surface == kind::module_) out.module_name = opt.module_name;
else out.include_dir = dir.string();
return out;
}
// Writes ONE half of the surface. The names come from `outputs`, which the
// caller has already asked; this produces the content.
//
// Nothing here reads a payload's BYTES. Under object storage the assembly names
// each payload in `.incbin` and the assembler reads it later, which is why this
// can be an action whose command runs before, after or independently of the
// payload's own producer -- what matters is that the payload is a declared
// INPUT of that action, and the graph then holds the edge.
inline bool write(std::span items, const options& opt, half which) {
if (items.empty()) return true;
if (opt.module_name.empty()) {
std::cerr << "mcpp.plugins.surface: options::module_name is required; it names both "
"the module a consumer imports and the namespace the declarations sit in\n";
return false;
}
if (opt.out_dir.empty()) {
std::cerr << "mcpp.plugins.surface: options::out_dir is required\n";
return false;
}
const auto segs = split_module_name(opt.module_name);
// REFUSED HERE, NOT IN THE GENERATED FILE. The module name is the one part
// of this surface a project writes itself, and each of its segments becomes
// a namespace. A segment that is not an identifier -- empty, starting with
// a digit, carrying a `-`, or reserved -- produces a generated file that
// does not parse, and the error then names a line nobody wrote.
for (std::size_t i = 0; i < segs.size(); ++i) {
if (segs[i] == identifier(segs[i], "")) continue;
std::cerr << std::format(
"mcpp.plugins.surface: `{}` is not a usable module name: the segment `{}` "
"is not a C++ identifier.\n Each segment becomes a namespace, so it has "
"to be one; `{}` would work.\n",
opt.module_name, segs[i], identifier(segs[i], "part"));
return false;
}
const auto by = opt.produced_by.empty() ? std::string("mcpp.plugins.surface")
: opt.produced_by;
// ONE derivation of the names, shared with the caller. Recomputing them
// here would be the second copy of a decision that this package has paid
// for before.
const emitted out = outputs(opt);
const auto dir = std::filesystem::path(opt.out_dir);
const auto body = declarations(items, opt);
// ---- interface ----
if (which == half::interface_) {
std::string iface;
iface += std::format("// Generated by mcpp.plugins.surface for {}. Do not edit.\n", by);
if (opt.surface == kind::module_)
iface += std::format("export module {};\n\n", opt.module_name);
else
iface += "#pragma once\n\n";
iface += extern_c_declarations(items, opt);
iface += "\n";
// `export` on the opening namespace exports everything the block
// contains, including the nested namespaces a payload's directory
// produced.
if (opt.surface == kind::module_) iface += "export ";
iface += open_namespaces(segs);
iface += "\n" + body + "\n";
iface += close_namespaces(segs);
if (!write_if_different(out.interface_file, iface)) {
std::cerr << std::format("mcpp.plugins.surface: cannot write {}\n",
out.interface_file);
return false;
}
return true;
}
// ---- implementation ---- (half::body from here down)
//
// The one translation unit that defines the accessors. A plain `.cpp` under
// every surface and every storage, because the accessors have C language
// linkage and so need no attachment to the module. What differs between the
// three storages is only where it reads the bytes from.
const char* elem = element_type(opt.elem);
std::string impl;
impl += std::format("// Generated by mcpp.plugins.surface for {}. Do not edit.\n", by);
if (out.store == storage::header) {
impl += "//\n"
"// The only translation unit that includes the generated data headers.\n"
"// Each declares a `static` array, so this is also the only copy of the\n"
"// bytes in the program.\n";
for (auto const& it : items) impl += std::format("#include \"{}\"\n", it.data_header);
impl += "\n";
impl += element_width_assertion(opt.elem);
impl += "\n";
for (auto const& it : items) {
const auto base = accessor_base(opt, it);
const auto size = it.data_size_expr.empty()
? std::format("sizeof {}", it.data_symbol)
: it.data_size_expr;
impl += std::format(
"extern \"C\" const {0}* {1}_data() {{ return {2}; }}\n"
"extern \"C\" unsigned long {1}_size() {{ return {3}; }}\n",
elem, base, it.data_symbol, size);
}
} else if (out.store == storage::object) {
impl += "//\n"
"// The bytes are in a section written by the generated `.S`. This file\n"
"// only names its two boundary symbols, so nothing here parses a payload\n"
"// and the C++ compiler never sees one.\n"
"//\n"
"// The symbols are declared as arrays of the element type rather than as\n"
"// `char`: the assembly aligned the section to four bytes, and a\n"
"// declaration that said `char` would let a consumer reach it through an\n"
"// under-aligned pointer with nothing to notice.\n";
impl += "\n";
impl += element_width_assertion(opt.elem);
impl += "\nextern \"C\" {\n";
for (auto const& it : items) {
const auto base = accessor_base(opt, it);
impl += std::format("extern const {0} {1}_begin[];\n"
"extern const {0} {1}_end[];\n", elem, base);
}
impl += "}\n\n";
for (auto const& it : items) {
const auto base = accessor_base(opt, it);
impl += std::format(
"extern \"C\" const {0}* {1}_data() {{ return {1}_begin; }}\n"
"extern \"C\" unsigned long {1}_size() {{\n"
" return static_cast(({1}_end - {1}_begin) * sizeof({0}));\n"
"}}\n", elem, base);
}
} else {
impl += "//\n"
"// The payloads are files beside the artifact, read on first use. The\n"
"// path is resolved against the WORKING DIRECTORY, which is the property\n"
"// that makes this storage the one a project opts into rather than the\n"
"// default: a program started from elsewhere finds nothing.\n"
"//\n"
"// Read once and kept: an accessor that reloaded would hand two callers\n"
"// two different pointers to the same payload, and a device API given\n"
"// the second after the first was freed is a defect with no message.\n";
impl += "#include \n#include \n#include \n\n";
impl += element_width_assertion(opt.elem);
impl += std::format(R"IMPL(
namespace {{
struct blob {{ {0}* data = nullptr; unsigned long size = 0; bool tried = false; }};
blob& load(const char* path, blob& b) {{
if (b.tried) return b;
b.tried = true;
std::FILE* f = std::fopen(path, "rb");
if (!f) {{
std::fprintf(stderr, "mcpp.plugins.surface: cannot open %s\n", path);
return b;
}}
std::fseek(f, 0, SEEK_END);
const long n = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
if (n > 0) {{
b.data = static_cast<{0}*>(std::malloc(static_cast(n)));
if (b.data && std::fread(b.data, 1, static_cast(n), f)
== static_cast(n))
b.size = static_cast(n);
else {{ std::free(b.data); b.data = nullptr; }}
}}
std::fclose(f);
return b;
}}
}} // namespace
)IMPL", elem);
impl += "\n";
for (auto const& it : items) {
const auto base = accessor_base(opt, it);
impl += std::format(
"static blob {0}_blob;\n"
"extern \"C\" const {1}* {0}_data() {{\n"
" return load(\"{2}\", {0}_blob).data;\n"
"}}\n"
"extern \"C\" unsigned long {0}_size() {{\n"
" return load(\"{2}\", {0}_blob).size;\n"
"}}\n", base, elem, it.sidecar_name);
}
}
if (!write_if_different(out.impl_file, impl)) {
std::cerr << std::format("mcpp.plugins.surface: cannot write {}\n", out.impl_file);
return false;
}
if (out.store == storage::object) {
// A CALLER THAT ASKS FOR THIS STORAGE MUST HAVE SAID WHERE THE BYTES
// ARE, AND THE REFUSAL IS WHAT KEEPS THAT TRUE.
//
// `item::payload_path` is documented as required under `object`, and
// today only `mcpp.rules.spirv` reaches this storage and only it sets
// the field. That is a COINCIDENCE, not a guarantee: the first caller
// to give `rules-slang` or `tools-embed` a storage option would get an
// `.incbin ""` in a generated `.S`, and the declaration below would
// then name an empty dependency -- so the build would be wrong in
// exactly the silent way this whole change exists to remove.
//
// A constraint written only in a comment has nothing enforcing it,
// which is a shape this project has recorded before. This is the
// enforcement.
for (auto const& it : items) {
if (!it.payload_path.empty()) continue;
std::cerr << std::format(
"mcpp.plugins.surface: `{}` was given storage::object with no "
"payload_path.\n"
" Under this storage the generated assembly names the payload "
"in `.incbin`, so\n"
" the caller has to say where it is. Set `item::payload_path`, "
"or use storage::header.\n",
it.identifier);
return false;
}
if (!write_if_different(out.assembly_file,
assembly_for(items, opt, opt.target_os))) {
std::cerr << std::format("mcpp.plugins.surface: cannot write {}\n",
out.assembly_file);
return false;
}
}
// `storage::sidecar` needs no counterpart, and that was checked rather than
// assumed. Its payload is never read by a compile: the accessor opens the
// file at RUN time, so a rebuilt payload is picked up by the next run with
// nothing in the build graph to keep current.
return true;
}
// The surface a project gets when it asks for nothing.
//
// `MCPP_LANGUAGE_MODULES` is set by mcpp from `[language] modules`. An engine
// that does not set it leaves the variable absent, and the fallback is the
// header surface -- which is what every consumer of this package had before
// this module existed. An older engine therefore keeps its behaviour and a
// newer one gets the module surface without any project asking, which is the
// whole of the upgrade path.
inline kind default_surface() {
const char* v = std::getenv("MCPP_LANGUAGE_MODULES");
return (v && (*v == '1' || *v == 't' || *v == 'T')) ? kind::module_ : kind::c_header;
}
// `example:my-app` -> `my_app`. The module a rule names by default is this
// followed by the group's own segment, so two packages in one build cannot
// claim the same module.
// The default module root for a package that named none: its PACKAGE NAME,
// sanitised into an identifier.
//
// THE PACKAGE'S NAME. NOT ITS DIRECTORY'S. These are different questions and
// they give different answers whenever a project lays a package out under a
// generic directory. The first version asked the only question mcpp could
// answer -- the leaf of MCPP_MANIFEST_DIR -- so a package named `vulkan-saxpy`
// laid out as `vulkan/app/` generated `app.shaders`, and every
// `/app/` in a workspace claimed that same module.
//
// The name ARRIVES rather than being read, for the reason `options::target_os`
// records: this module must compile into a plain binary as well as into a
// build program, so it may not reach for `mcpp::package_name()` itself. The
// caller passes what mcpp told it.
//
// `fallback` covers the impossible case -- a manifest without a `[package]
// name` does not load, so an empty first argument would mean the contract
// changed underneath. A rule that wants neither passes `options::module_name`,
// and this is not consulted.
inline std::string module_root_for(std::string_view package_name,
std::string_view fallback = {}) {
std::string leaf{package_name};
if (leaf.empty()) leaf = std::string(fallback);
return identifier(leaf, "app");
}
} // namespace mcpp::plugins::surface