See More

// 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