This commit is contained in:
parent
dea67ae5aa
commit
c466d90eec
4 changed files with 386 additions and 24 deletions
|
|
@ -471,7 +471,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
// -mllvm is consumed by codegen but not the link driver, which is the
|
// -mllvm is consumed by codegen but not the link driver, which is the
|
||||||
// same command line; quiet the unused-flag warning rather than split
|
// same command line; quiet the unused-flag warning rather than split
|
||||||
// compile and link commands.
|
// compile and link commands.
|
||||||
command += " -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument";
|
command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument";
|
||||||
}
|
}
|
||||||
if (config.target == "x86_64-w64-mingw32") {
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
|
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
|
||||||
|
|
@ -621,16 +621,32 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
command += " -O3";
|
command += " -O3";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same target-aware setup as the C++ compile path (line 459-): wasm32
|
||||||
|
// rejects -march, silently ignores -mtune, and needs --sysroot to find
|
||||||
|
// wasi-libc headers. Build the prefix once so all C compiles share it.
|
||||||
|
const bool cIsWasm = config.target.starts_with("wasm32");
|
||||||
|
std::string cArchFlags = cIsWasm
|
||||||
|
? std::string()
|
||||||
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||||
|
if (!config.sysroot.empty()) {
|
||||||
|
cArchFlags += std::format(" --sysroot={}", config.sysroot);
|
||||||
|
}
|
||||||
|
if (cIsWasm) {
|
||||||
|
// Matches the C++ path's wasi flag set so any libc shim defines
|
||||||
|
// (e.g. _WASI_EMULATED_SIGNAL → signal.h shims) are visible to
|
||||||
|
// C dependencies that drag in signal.h transitively.
|
||||||
|
cArchFlags += " -D_WASI_EMULATED_SIGNAL";
|
||||||
|
}
|
||||||
for (const fs::path& cFile : config.cFiles) {
|
for (const fs::path& cFile : config.cFiles) {
|
||||||
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
|
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
|
||||||
const std::string objPath = (buildDir / cFile.filename()).string() + "_source.o";
|
const std::string objPath = (buildDir / cFile.filename()).string() + "_source.o";
|
||||||
const std::string srcPath = cFile.string() + ".c";
|
const std::string srcPath = cFile.string() + ".c";
|
||||||
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
|
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
|
||||||
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config, &includeFlags, &defineFlags, &userFlags]() {
|
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config, &includeFlags, &defineFlags, &userFlags, &cArchFlags]() {
|
||||||
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
|
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
|
||||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||||
|
|
||||||
std::string result = RunCommand(std::format("clang {}.c --target={} -march={} -mtune={} -O3 -c{}{}{} -o {}_source.o", cFile.string(), config.target, config.march, config.mtune, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
|
std::string result = RunCommand(std::format("clang {}.c --target={}{} -O3 -c{}{}{} -o {}_source.o", cFile.string(), config.target, cArchFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
|
||||||
if (result.empty()) return;
|
if (result.empty()) return;
|
||||||
|
|
||||||
bool expected = false;
|
bool expected = false;
|
||||||
|
|
@ -1034,16 +1050,79 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
||||||
fs::create_directories(htmlOutDir);
|
fs::create_directories(htmlOutDir);
|
||||||
fs::path htmlPath = htmlOutDir / "index.html";
|
fs::path htmlPath = htmlOutDir / "index.html";
|
||||||
|
|
||||||
|
// Walk the dep graph for env-style JS bridges that need to load BEFORE
|
||||||
|
// runtime.js so they can populate `window.crafter_webbuild_env`. Any
|
||||||
|
// `*.js` entry in a (transitive) dep's `cfg.files` qualifies — the
|
||||||
|
// file is already going to be copied into the consumer's bin dir, we
|
||||||
|
// just need its basename for a `<script src=...>` tag.
|
||||||
|
std::vector<std::string> envScripts;
|
||||||
|
std::unordered_set<Configuration*> seen;
|
||||||
|
std::function<void(Configuration*)> walk = [&](Configuration* c) {
|
||||||
|
if (!c || !seen.insert(c).second) return;
|
||||||
|
for (const fs::path& f : c->files) {
|
||||||
|
if (f.extension() == ".js" && f.filename() != "runtime.js") {
|
||||||
|
std::string name = f.filename().string();
|
||||||
|
if (std::find(envScripts.begin(), envScripts.end(), name) == envScripts.end()) {
|
||||||
|
envScripts.push_back(std::move(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Configuration* dep : c->dependencies) walk(dep);
|
||||||
|
};
|
||||||
|
walk(&cfg);
|
||||||
|
|
||||||
|
std::string envScriptTags;
|
||||||
|
for (const std::string& name : envScripts) {
|
||||||
|
envScriptTags += std::format(" <script src=\"{}\" type=\"module\"></script>\n", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the dep graph again for non-JS assets — these get pre-loaded by
|
||||||
|
// runtime.js into an in-memory VFS so the wasm's std::ifstream et al.
|
||||||
|
// can actually read them (the wasi-runtime in this repo otherwise
|
||||||
|
// stubs every fd syscall to zero). The manifest lists basenames only;
|
||||||
|
// each file lives next to the .wasm in the bin dir.
|
||||||
|
std::vector<std::string> assetFiles;
|
||||||
|
seen.clear();
|
||||||
|
std::function<void(Configuration*)> walkAssets = [&](Configuration* c) {
|
||||||
|
if (!c || !seen.insert(c).second) return;
|
||||||
|
for (const fs::path& f : c->files) {
|
||||||
|
std::string ext = f.extension().string();
|
||||||
|
if (ext == ".js" || ext == ".html") continue;
|
||||||
|
if (f.filename() == "runtime.js") continue;
|
||||||
|
std::string name = f.filename().string();
|
||||||
|
if (name.empty()) continue;
|
||||||
|
if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) {
|
||||||
|
assetFiles.push_back(std::move(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Configuration* dep : c->dependencies) walkAssets(dep);
|
||||||
|
};
|
||||||
|
walkAssets(&cfg);
|
||||||
|
|
||||||
|
fs::path manifestPath = htmlOutDir / "files.json";
|
||||||
|
{
|
||||||
|
std::ofstream m(manifestPath);
|
||||||
|
m << "[";
|
||||||
|
for (std::size_t i = 0; i < assetFiles.size(); ++i) {
|
||||||
|
if (i) m << ",";
|
||||||
|
m << "\"" << assetFiles[i] << "\"";
|
||||||
|
}
|
||||||
|
m << "]";
|
||||||
|
}
|
||||||
|
|
||||||
std::ifstream in(htmlTemplate);
|
std::ifstream in(htmlTemplate);
|
||||||
std::stringstream buf;
|
std::stringstream buf;
|
||||||
buf << in.rdbuf();
|
buf << in.rdbuf();
|
||||||
std::string html = std::regex_replace(buf.str(), std::regex(R"(\{\{WASM\}\})"), cfg.outputName + ".wasm");
|
std::string html = buf.str();
|
||||||
|
html = std::regex_replace(html, std::regex(R"(\{\{WASM\}\})"), cfg.outputName + ".wasm");
|
||||||
|
html = std::regex_replace(html, std::regex(R"(\{\{ENV_SCRIPTS\}\})"), envScriptTags);
|
||||||
std::ofstream out(htmlPath);
|
std::ofstream out(htmlPath);
|
||||||
out << html;
|
out << html;
|
||||||
out.close();
|
out.close();
|
||||||
|
|
||||||
cfg.files.push_back(runtimeJs);
|
cfg.files.push_back(runtimeJs);
|
||||||
cfg.files.push_back(htmlPath);
|
cfg.files.push_back(htmlPath);
|
||||||
|
cfg.files.push_back(manifestPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Crafter::HostTarget() {
|
std::string Crafter::HostTarget() {
|
||||||
|
|
@ -1087,6 +1166,15 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
|
||||||
}
|
}
|
||||||
if (sawLib && cfg.type == ConfigurationType::Executable) cfg.type = ConfigurationType::LibraryStatic;
|
if (sawLib && cfg.type == ConfigurationType::Executable) cfg.type = ConfigurationType::LibraryStatic;
|
||||||
if (sawShared && cfg.type == ConfigurationType::LibraryStatic) cfg.type = ConfigurationType::LibraryDynamic;
|
if (sawShared && cfg.type == ConfigurationType::LibraryStatic) cfg.type = ConfigurationType::LibraryDynamic;
|
||||||
|
// WASI sysroot autodetect, applied at config-load time so the VariantId
|
||||||
|
// includes it. (Build() also runs this once more for callers that bypassed
|
||||||
|
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
||||||
|
// between the consumer's command-construction and the dep's own build.)
|
||||||
|
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||||
|
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
||||||
|
cfg.sysroot = "/usr/share/wasi-sysroot";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return ArgQuery{args};
|
return ArgQuery{args};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1234,11 +1322,76 @@ int Crafter::Run(int argc, char** argv) {
|
||||||
} else if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
|
} else if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
|
||||||
artifact += ".exe";
|
artifact += ".exe";
|
||||||
}
|
}
|
||||||
|
artifact = fs::absolute(artifact);
|
||||||
|
fs::path absDir = fs::absolute(dir);
|
||||||
|
|
||||||
|
// wasm targets need either a wasm runtime (wasi-cli) or an HTTP
|
||||||
|
// server (browser build with index.html). std::system on the
|
||||||
|
// .wasm path goes nowhere useful — replace with detection.
|
||||||
|
if (config.target.starts_with("wasm32")) {
|
||||||
|
bool browserBuild = fs::exists(absDir / "index.html");
|
||||||
|
auto have = [](std::string_view exe) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string probe = std::format("where {} > NUL 2>&1", exe);
|
||||||
|
#else
|
||||||
|
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
|
||||||
|
#endif
|
||||||
|
return std::system(probe.c_str()) == 0;
|
||||||
|
};
|
||||||
|
if (browserBuild) {
|
||||||
|
// Try installed HTTP servers in priority order: lightweight
|
||||||
|
// / dependency-free first, ad-hoc ones last. Foreground
|
||||||
|
// (Ctrl-C to stop); we exec, not fork.
|
||||||
|
const int port = 8080;
|
||||||
|
std::string cmd;
|
||||||
|
std::string_view picked;
|
||||||
|
if (have("caddy")) {
|
||||||
|
picked = "caddy";
|
||||||
|
cmd = std::format("caddy file-server --listen :{} --root {}", port, absDir.string());
|
||||||
|
} else if (have("python3")) {
|
||||||
|
picked = "python3";
|
||||||
|
cmd = std::format("python3 -m http.server --directory {} {}", absDir.string(), port);
|
||||||
|
} else if (have("python")) {
|
||||||
|
picked = "python";
|
||||||
|
cmd = std::format("python -m http.server --directory {} {}", absDir.string(), port);
|
||||||
|
} else if (have("php")) {
|
||||||
|
picked = "php";
|
||||||
|
cmd = std::format("php -S 0.0.0.0:{} -t {}", port, absDir.string());
|
||||||
|
} else if (have("ruby")) {
|
||||||
|
picked = "ruby";
|
||||||
|
cmd = std::format("ruby -run -e httpd {} -p{}", absDir.string(), port);
|
||||||
|
} else if (have("busybox")) {
|
||||||
|
picked = "busybox httpd";
|
||||||
|
cmd = std::format("busybox httpd -f -p {} -h {}", port, absDir.string());
|
||||||
|
} else if (have("npx")) {
|
||||||
|
picked = "npx http-server";
|
||||||
|
cmd = std::format("npx --yes http-server {} -p {} --silent", absDir.string(), port);
|
||||||
|
} else {
|
||||||
|
std::println(std::cerr,
|
||||||
|
"-r wasm: no HTTP server found in PATH. Install one of: "
|
||||||
|
"caddy, python3, python, php, ruby, busybox, npx (Node.js).");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::println("serving {} via {} at http://localhost:{}/", absDir.string(), picked, port);
|
||||||
|
return std::system(cmd.c_str()) == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
// wasi-cli wasm — needs a standalone runtime.
|
||||||
|
if (have("wasmtime")) {
|
||||||
|
return std::system(std::format("wasmtime {}", artifact.string()).c_str()) == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
if (have("wasmer")) {
|
||||||
|
return std::system(std::format("wasmer run {}", artifact.string()).c_str()) == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
std::println(std::cerr,
|
||||||
|
"-r wasm: no wasm runtime found in PATH. Install wasmtime or wasmer, "
|
||||||
|
"or call EnableWasiBrowserRuntime(cfg) in project.cpp for a browser build.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve to absolute — cmd.exe on Windows mishandles a leading
|
// Resolve to absolute — cmd.exe on Windows mishandles a leading
|
||||||
// "./" by trying to interpret it as a command. system() invokes
|
// "./" by trying to interpret it as a command. system() invokes
|
||||||
// through cmd /c, so the relative-prefixed path makes cmd error
|
// through cmd /c, so the relative-prefixed path makes cmd error
|
||||||
// with "'.' is not recognized as an internal or external command".
|
// with "'.' is not recognized as an internal or external command".
|
||||||
artifact = fs::absolute(artifact);
|
|
||||||
// Run from the artifact's own directory so relative file opens
|
// Run from the artifact's own directory so relative file opens
|
||||||
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
||||||
// against the bin dir rather than the user's cwd. We exit the
|
// against the bin dir rather than the user's cwd. We exit the
|
||||||
|
|
|
||||||
|
|
@ -705,7 +705,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
// EH); signal.h requires the emulation define; and EH itself isn't
|
// EH); signal.h requires the emulation define; and EH itself isn't
|
||||||
// wired up so -fno-exceptions stays.
|
// wired up so -fno-exceptions stays.
|
||||||
std::string archFlags = isWasm
|
std::string archFlags = isWasm
|
||||||
? std::string(" -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
? std::string(" -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
||||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
||||||
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{WASM}}</title>
|
<title>{{WASM}}</title>
|
||||||
<script>window.CRAFTER_WASM_URL = "{{WASM}}";</script>
|
<script>window.CRAFTER_WASM_URL = "{{WASM}}";</script>
|
||||||
<script src="runtime.js" type="module"></script>
|
{{ENV_SCRIPTS}} <script src="runtime.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin:0;"></body>
|
<body style="margin:0;"></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,41 @@ class Wasi {
|
||||||
#envEncodedStrings;
|
#envEncodedStrings;
|
||||||
#argEncodedStrings;
|
#argEncodedStrings;
|
||||||
instance;
|
instance;
|
||||||
|
// VFS: file basename → Uint8Array, populated by EnableWasiBrowserRuntime's
|
||||||
|
// preload step before instantiateStreaming runs. Used by path_open / fd_read
|
||||||
|
// / fd_seek / fd_tell / fd_filestat_get / fd_close so wasi-libc's
|
||||||
|
// std::ifstream and friends actually work in the browser.
|
||||||
|
vfs;
|
||||||
|
#fdTable;
|
||||||
|
#nextFd;
|
||||||
|
#decoder;
|
||||||
|
|
||||||
constructor({ env, stdin, args }) {
|
constructor({ env, stdin, args, vfs }) {
|
||||||
this.#encodedStdin = textEncoder.encode(stdin);
|
this.#encodedStdin = textEncoder.encode(stdin);
|
||||||
const envStrings = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
const envStrings = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
||||||
this.#envEncodedStrings = envStrings.map(s => textEncoder.encode(s + "\0"));
|
this.#envEncodedStrings = envStrings.map(s => textEncoder.encode(s + "\0"));
|
||||||
this.#argEncodedStrings = args.map(s => textEncoder.encode(s + "\0"));
|
this.#argEncodedStrings = args.map(s => textEncoder.encode(s + "\0"));
|
||||||
|
this.vfs = vfs || new Map();
|
||||||
|
this.#fdTable = new Map();
|
||||||
|
this.#nextFd = 100; // stay clear of stdio
|
||||||
|
this.#decoder = new TextDecoder();
|
||||||
this.bind();
|
this.bind();
|
||||||
}
|
}
|
||||||
|
|
||||||
bind() {
|
bind() {
|
||||||
this.args_get = this.args_get.bind(this);
|
// wasi imports are looked up as plain function references at
|
||||||
this.args_sizes_get = this.args_sizes_get.bind(this);
|
// instantiate time, so any method that touches `this` MUST be
|
||||||
this.environ_get = this.environ_get.bind(this);
|
// explicitly bound here. Anything purely no-op (returning 0) can
|
||||||
this.environ_sizes_get = this.environ_sizes_get.bind(this);
|
// stay unbound.
|
||||||
this.fd_read = this.fd_read.bind(this);
|
const m = [
|
||||||
this.fd_write = this.fd_write.bind(this);
|
"args_get", "args_sizes_get",
|
||||||
|
"environ_get", "environ_sizes_get",
|
||||||
|
"fd_read", "fd_write", "fd_close", "fd_seek", "fd_tell",
|
||||||
|
"fd_filestat_get", "fd_fdstat_get",
|
||||||
|
"fd_prestat_get", "fd_prestat_dir_name",
|
||||||
|
"path_open",
|
||||||
|
];
|
||||||
|
for (const name of m) this[name] = this[name].bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
args_sizes_get(argCountPtr, argBufferSizePtr) {
|
args_sizes_get(argCountPtr, argBufferSizePtr) {
|
||||||
|
|
@ -71,6 +90,7 @@ class Wasi {
|
||||||
fd_read(fd, iovsPtr, iovsLength, bytesReadPtr) {
|
fd_read(fd, iovsPtr, iovsLength, bytesReadPtr) {
|
||||||
const memory = new Uint8Array(this.instance.exports.memory.buffer);
|
const memory = new Uint8Array(this.instance.exports.memory.buffer);
|
||||||
const iovs = new Uint32Array(this.instance.exports.memory.buffer, iovsPtr, iovsLength * 2);
|
const iovs = new Uint32Array(this.instance.exports.memory.buffer, iovsPtr, iovsLength * 2);
|
||||||
|
const dataView = new DataView(this.instance.exports.memory.buffer);
|
||||||
let totalBytesRead = 0;
|
let totalBytesRead = 0;
|
||||||
if (fd === 0) {
|
if (fd === 0) {
|
||||||
for (let i = 0; i < iovsLength * 2; i += 2) {
|
for (let i = 0; i < iovsLength * 2; i += 2) {
|
||||||
|
|
@ -82,24 +102,120 @@ class Wasi {
|
||||||
totalBytesRead += chunk.byteLength;
|
totalBytesRead += chunk.byteLength;
|
||||||
if (this.#encodedStdin.length === 0) break;
|
if (this.#encodedStdin.length === 0) break;
|
||||||
}
|
}
|
||||||
const dataView = new DataView(this.instance.exports.memory.buffer);
|
|
||||||
dataView.setInt32(bytesReadPtr, totalBytesRead, true);
|
dataView.setInt32(bytesReadPtr, totalBytesRead, true);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
const entry = this.#fdTable.get(fd);
|
||||||
|
if (!entry) {
|
||||||
|
dataView.setInt32(bytesReadPtr, 0, true);
|
||||||
|
return 8; // EBADF
|
||||||
|
}
|
||||||
|
const file = this.vfs.get(entry.name);
|
||||||
|
for (let i = 0; i < iovsLength * 2; i += 2) {
|
||||||
|
const offset = iovs[i];
|
||||||
|
const length = iovs[i + 1];
|
||||||
|
const remaining = file.byteLength - entry.offset;
|
||||||
|
if (remaining <= 0) break;
|
||||||
|
const n = Math.min(length, remaining);
|
||||||
|
memory.set(file.subarray(entry.offset, entry.offset + n), offset);
|
||||||
|
entry.offset += n;
|
||||||
|
totalBytesRead += n;
|
||||||
|
if (n < length) break;
|
||||||
|
}
|
||||||
|
dataView.setInt32(bytesReadPtr, totalBytesRead, true);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
fd_advise() { return 0; }
|
fd_advise() { return 0; }
|
||||||
fd_close() { return 0; }
|
fd_close(fd) {
|
||||||
fd_fdstat_get() { return 0; }
|
this.#fdTable.delete(fd);
|
||||||
fd_prestat_get() { return 0; }
|
return 0;
|
||||||
fd_prestat_dir_name() { return 0; }
|
}
|
||||||
|
fd_fdstat_get(fd, statPtr) {
|
||||||
|
// 24 bytes: filetype(1) + flags(2) + padding + rights_base(8) + rights_inheriting(8).
|
||||||
|
const dv = new DataView(this.instance.exports.memory.buffer);
|
||||||
|
const isFile = this.#fdTable.has(fd);
|
||||||
|
const filetype = isFile ? 4 : 0; // 4 = regular_file
|
||||||
|
dv.setUint8(statPtr + 0, filetype);
|
||||||
|
dv.setUint16(statPtr + 2, 0, true);
|
||||||
|
dv.setBigUint64(statPtr + 8, 0xFFFFFFFFFFFFFFFFn, true);
|
||||||
|
dv.setBigUint64(statPtr + 16, 0xFFFFFFFFFFFFFFFFn, true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// wasi-libc walks preopens starting at fd=3 until fd_prestat_get returns
|
||||||
|
// EBADF, then resolves every relative open against one of the discovered
|
||||||
|
// dirs. Without at least one preopen, std::ifstream et al can't open any
|
||||||
|
// path. We expose a single "/" preopen on fd=3, rooted at the VFS map.
|
||||||
|
fd_prestat_get(fd, prestatPtr) {
|
||||||
|
const dv = new DataView(this.instance.exports.memory.buffer);
|
||||||
|
if (fd === 3) {
|
||||||
|
// prestat_t: tag(u8) + padding to 4 + u.dir.pr_name_len(u32). 8 bytes.
|
||||||
|
dv.setUint8(prestatPtr + 0, 0); // tag = preopentype_dir
|
||||||
|
dv.setUint32(prestatPtr + 4, 1, true); // strlen("/")
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 8; // EBADF
|
||||||
|
}
|
||||||
|
fd_prestat_dir_name(fd, pathPtr, pathLen) {
|
||||||
|
if (fd !== 3) return 8;
|
||||||
|
const memory = new Uint8Array(this.instance.exports.memory.buffer);
|
||||||
|
const name = textEncoder.encode("/");
|
||||||
|
memory.set(name.subarray(0, Math.min(name.byteLength, pathLen)), pathPtr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
clock_res_get() { return 0; }
|
clock_res_get() { return 0; }
|
||||||
clock_time_get() { return 0; }
|
clock_time_get() { return 0; }
|
||||||
fd_seek() { return 0; }
|
fd_seek(fd, offsetLow, whence, newOffsetPtr) {
|
||||||
|
// offsetLow is a BigInt under wasi-snapshot-preview1's two-i32 ABI?
|
||||||
|
// No — wasi-snapshot-preview1 fd_seek's signature is (fd, filedelta:i64,
|
||||||
|
// whence:u8, newoffset:ptr<filesize>). JS receives the i64 as BigInt.
|
||||||
|
const entry = this.#fdTable.get(fd);
|
||||||
|
const dv = new DataView(this.instance.exports.memory.buffer);
|
||||||
|
if (!entry) {
|
||||||
|
dv.setBigUint64(newOffsetPtr, 0n, true);
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
const delta = typeof offsetLow === "bigint" ? Number(offsetLow) : Number(offsetLow);
|
||||||
|
const file = this.vfs.get(entry.name);
|
||||||
|
let newOff;
|
||||||
|
switch (whence) {
|
||||||
|
case 0: newOff = delta; break; // SEEK_SET
|
||||||
|
case 1: newOff = entry.offset + delta; break; // SEEK_CUR
|
||||||
|
case 2: newOff = file.byteLength + delta; break; // SEEK_END
|
||||||
|
default: return 28; // EINVAL
|
||||||
|
}
|
||||||
|
if (newOff < 0) newOff = 0;
|
||||||
|
if (newOff > file.byteLength) newOff = file.byteLength;
|
||||||
|
entry.offset = newOff;
|
||||||
|
dv.setBigUint64(newOffsetPtr, BigInt(newOff), true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
fd_tell(fd, offsetPtr) {
|
||||||
|
const entry = this.#fdTable.get(fd);
|
||||||
|
const dv = new DataView(this.instance.exports.memory.buffer);
|
||||||
|
if (!entry) { dv.setBigUint64(offsetPtr, 0n, true); return 8; }
|
||||||
|
dv.setBigUint64(offsetPtr, BigInt(entry.offset), true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
fd_allocate() { return 0; }
|
fd_allocate() { return 0; }
|
||||||
fd_datasync() { return 0; }
|
fd_datasync() { return 0; }
|
||||||
fd_fdstat_set_flags() { return 0; }
|
fd_fdstat_set_flags() { return 0; }
|
||||||
fd_fdstat_set_rights() { return 0; }
|
fd_fdstat_set_rights() { return 0; }
|
||||||
fd_filestat_get() { return 0; }
|
fd_filestat_get(fd, statPtr) {
|
||||||
|
// Layout: dev(8) ino(8) filetype(1) +pad nlink(8) size(8) atim(8) mtim(8) ctim(8) = 64 bytes.
|
||||||
|
const dv = new DataView(this.instance.exports.memory.buffer);
|
||||||
|
const entry = this.#fdTable.get(fd);
|
||||||
|
if (!entry) return 8;
|
||||||
|
const file = this.vfs.get(entry.name);
|
||||||
|
dv.setBigUint64(statPtr + 0, 0n, true);
|
||||||
|
dv.setBigUint64(statPtr + 8, 0n, true);
|
||||||
|
dv.setUint8(statPtr + 16, 4); // filetype = regular_file
|
||||||
|
dv.setBigUint64(statPtr + 24, 1n, true);
|
||||||
|
dv.setBigUint64(statPtr + 32, BigInt(file.byteLength), true);
|
||||||
|
dv.setBigUint64(statPtr + 40, 0n, true);
|
||||||
|
dv.setBigUint64(statPtr + 48, 0n, true);
|
||||||
|
dv.setBigUint64(statPtr + 56, 0n, true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
fd_filestat_set_size() { return 0; }
|
fd_filestat_set_size() { return 0; }
|
||||||
fd_filestat_set_times() { return 0; }
|
fd_filestat_set_times() { return 0; }
|
||||||
fd_pread() { return 0; }
|
fd_pread() { return 0; }
|
||||||
|
|
@ -112,7 +228,22 @@ class Wasi {
|
||||||
path_filestat_get() { return 0; }
|
path_filestat_get() { return 0; }
|
||||||
path_filestat_set_times() { return 0; }
|
path_filestat_set_times() { return 0; }
|
||||||
path_link() { return 0; }
|
path_link() { return 0; }
|
||||||
path_open() { return 0; }
|
path_open(_dirfd, _dirflags, pathPtr, pathLen, _oflags, _rightsBase, _rightsInh, _fdflags, openedFdPtr) {
|
||||||
|
const memory = new Uint8Array(this.instance.exports.memory.buffer);
|
||||||
|
const dv = new DataView(this.instance.exports.memory.buffer);
|
||||||
|
const raw = this.#decoder.decode(memory.subarray(pathPtr, pathPtr + pathLen));
|
||||||
|
// wasi-libc may pass paths like "./font.ttf" or "/font.ttf" or "font.ttf"
|
||||||
|
// depending on how std::filesystem::path was constructed. Reduce to basename.
|
||||||
|
const base = raw.split(/[\\/]/).filter(Boolean).pop() || raw;
|
||||||
|
if (!this.vfs.has(base)) {
|
||||||
|
dv.setInt32(openedFdPtr, -1, true);
|
||||||
|
return 44; // ENOENT
|
||||||
|
}
|
||||||
|
const fd = this.#nextFd++;
|
||||||
|
this.#fdTable.set(fd, { name: base, offset: 0 });
|
||||||
|
dv.setInt32(openedFdPtr, fd, true);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
path_readlink() { return 0; }
|
path_readlink() { return 0; }
|
||||||
path_remove_directory() { return 0; }
|
path_remove_directory() { return 0; }
|
||||||
path_rename() { return 0; }
|
path_rename() { return 0; }
|
||||||
|
|
@ -147,7 +278,17 @@ class Wasi {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
proc_exit(code) {
|
proc_exit(code) {
|
||||||
|
// Throw a sentinel so the wasm stack unwinds back to runtime.js
|
||||||
|
// WITHOUT executing the `unreachable` instruction wasi-libc emits
|
||||||
|
// after __wasi_proc_exit (which is declared noreturn). Trapping
|
||||||
|
// there would mark the wasm call as crashed and may interrupt any
|
||||||
|
// browser-side render loop that called _Exit on purpose (e.g.
|
||||||
|
// Crafter::Window::StartSync on DOM, which hands the loop to rAF
|
||||||
|
// and then exits main without running static destructors).
|
||||||
console.log(`[wasi] proc_exit(${code})`);
|
console.log(`[wasi] proc_exit(${code})`);
|
||||||
|
const e = new Error(`wasi proc_exit(${code})`);
|
||||||
|
e.crafterWasiExit = code;
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,12 +297,80 @@ if (!wasmUrl) {
|
||||||
throw new Error("runtime.js: window.CRAFTER_WASM_URL is not set (set it in index.html before loading runtime.js)");
|
throw new Error("runtime.js: window.CRAFTER_WASM_URL is not set (set it in index.html before loading runtime.js)");
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasi = new Wasi({ stdin: "", env: {}, args: [] });
|
// Preload asset files listed in files.json (emitted by
|
||||||
|
// EnableWasiBrowserRuntime) into an in-memory VFS so wasi-libc's file
|
||||||
|
// syscalls work against them. Browser builds otherwise can't open assets
|
||||||
|
// shipped alongside the .wasm — sync XHR is too deprecated to rely on.
|
||||||
|
const vfs = new Map();
|
||||||
|
try {
|
||||||
|
const manifestResp = await fetch("files.json");
|
||||||
|
if (manifestResp.ok) {
|
||||||
|
const names = await manifestResp.json();
|
||||||
|
await Promise.all(names.map(async (name) => {
|
||||||
|
const r = await fetch(name);
|
||||||
|
if (r.ok) vfs.set(name, new Uint8Array(await r.arrayBuffer()));
|
||||||
|
else console.warn(`[wasi] failed to preload ${name}: HTTP ${r.status}`);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[wasi] no files.json manifest (or fetch failed); file I/O syscalls will return ENOENT:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasi = new Wasi({ stdin: "", env: {}, args: [], vfs });
|
||||||
|
|
||||||
|
// Modules that need env imports (Crafter.Graphics DOM mode, etc.) ship a
|
||||||
|
// co-located env.js that sets `window.crafter_webbuild_env`. The
|
||||||
|
// `EnableWasiBrowserRuntime` injects each one as a regular <script> tag
|
||||||
|
// before this module script, so by the time we instantiate they've all
|
||||||
|
// populated the global. Pure-WASI builds leave the global undefined and
|
||||||
|
// we pass an empty object — extra imports on the JS side are harmless
|
||||||
|
// (WebAssembly only complains about *missing* declared imports).
|
||||||
|
if (!window.crafter_webbuild_env) {
|
||||||
|
window.crafter_webbuild_env = {};
|
||||||
|
}
|
||||||
|
// Some env.js bridges (notably the Crafter.CppDOM one) expect an indirect
|
||||||
|
// function table they can push function references onto. Set it up so we
|
||||||
|
// don't break compatibility with that pattern.
|
||||||
|
window.crafter_webbuild_env.table = new WebAssembly.Table({ initial: 4, element: "anyfunc" });
|
||||||
|
|
||||||
|
// Async env bridges (notably the WebGPU one in Crafter.Graphics) set
|
||||||
|
// `window.crafter_webbuild_env_ready` to a Promise that resolves once
|
||||||
|
// their init (adapter/device requests, pipeline compilation, etc.) is
|
||||||
|
// finished AND env.* has been swapped from sync stubs to real impls.
|
||||||
|
// We MUST await this BEFORE WebAssembly.instantiateStreaming — the wasm
|
||||||
|
// snapshots its import functions at instantiate time, so stubs captured
|
||||||
|
// then are baked in. Sibling <script type="module"> top-level awaits
|
||||||
|
// aren't reliably serialized cross-browser, so we can't rely on dom-*
|
||||||
|
// scripts' own TLAs to block us here.
|
||||||
|
if (window.crafter_webbuild_env_ready) {
|
||||||
|
try {
|
||||||
|
await window.crafter_webbuild_env_ready;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[crafter] env bridge init failed:", e);
|
||||||
|
// Continue to instantiation anyway — the bridge's stubs throw a
|
||||||
|
// clearer error from their own call sites.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
|
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
|
||||||
wasi_snapshot_preview1: wasi,
|
wasi_snapshot_preview1: wasi,
|
||||||
|
env: window.crafter_webbuild_env,
|
||||||
});
|
});
|
||||||
wasi.instance = instance;
|
wasi.instance = instance;
|
||||||
|
// Alias under both names — `crafter_wasi` is the canonical, but the older
|
||||||
|
// CppDOM env.js reads from `crafter_webbuild_wasi`. Keep both wired so
|
||||||
|
// either generation of env.js works against this runtime.
|
||||||
window.crafter_wasi = wasi;
|
window.crafter_wasi = wasi;
|
||||||
|
window.crafter_webbuild_wasi = wasi;
|
||||||
|
|
||||||
|
try {
|
||||||
instance.exports._start();
|
instance.exports._start();
|
||||||
|
} catch (e) {
|
||||||
|
if (e && typeof e.crafterWasiExit === "number") {
|
||||||
|
// Clean exit from std::_Exit / __wasi_proc_exit. The wasm instance
|
||||||
|
// remains valid; any rAF callback registered before exit keeps
|
||||||
|
// running. (See Crafter::Window::StartSync DOM impl.)
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue