diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cb71f2..e925c1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ name: CI # (checks/hosted; mcpp docs/24) # platform a platform module used outside `if constexpr' for its platform # fails to LINK, and the same use in a discarded branch links (R5) +# emscripten the tests that start no program, built for wasm32-emscripten and +# run under node (checks/emscripten); a program that starts one +# fails at LINK naming the openkal symbol (checks/absent) # # The library is built with the released mcpp, 2026.10.26.2 or later: the # first that compiles a unit with a module map importing another package's @@ -324,3 +327,75 @@ jobs: echo "::endgroup::" done exit $failed + + # ── EMSCRIPTEN (`wasm32-emscripten`) ──────────────────────────────────────── + # + # A wasm module over Emscripten's C library and openkal-emscripten, run under + # node. The toolchain is the target's (emsdk 6.0.9) and the runner is node + # from the payload's descriptor. openkal-emscripten has no openkal.process, so + # the two suites that start programs are not built; that a program which + # starts one fails at link, naming the symbol, is asserted instead. + emscripten: + name: emscripten (node) ${{ matrix.profile }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + profile: [dev, release] + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - name: Install xlings and mcpp + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.xlings/subos/current/bin:$PATH" + for attempt in 1 2 3 4 5 6; do + xlings update > /dev/null 2>&1 || true + if xlings install "mcpp@$MCPP_VERSION" -y -g; then break; fi + [ "$attempt" = 6 ] && { echo "::error::mcpp could not be installed"; exit 1; } + sleep 60 + done + mcpp --version + mcpp self config --mirror GLOBAL + if [ -n "${MCPP_SOURCE_REF:-}" ]; then + src="$RUNNER_TEMP/mcpp-src" + git clone --quiet --depth 1 --branch "$MCPP_SOURCE_REF" https://github.com/mcpp-community/mcpp.git "$src" + rm -f "$src/.xlings.json" + ( cd "$src" && mcpp build --release ) + built=$(find "$src/target" -type f -name mcpp | head -1) + [ -n "$built" ] || { echo "::error::mcpp did not build"; exit 1; } + echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" + fi + - name: Every test of the package that starts no program has its counterpart here + run: | + for t in tests/*_test.cpp; do + n=$(basename "$t") + case "$n" in process_test.cpp|ipc_test.cpp) continue ;; esac + [ -f "checks/emscripten/tests/$n" ] || { echo "::error::checks/emscripten/tests has no $n"; exit 1; } + done + - name: Tests, under node + working-directory: checks/emscripten + run: | + set -euo pipefail + mcpp test --target wasm32-emscripten --profile '${{ matrix.profile }}' 2>&1 | tee tests.log + for name in platform_test fs_test dirs_terminal_test linux_test library_test; do + grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; } + done + - name: The compile-time constants of Emscripten (R3) + run: ( cd checks/constants && mcpp build --target wasm32-emscripten ) || { echo "::error::a constant does not hold"; exit 1; } + - name: A program that names itself fails at link, naming the openkal symbol + run: | + if ( cd checks/absent && mcpp build --target wasm32-emscripten ) > absent.log 2>&1; then + cat absent.log + echo "::error::a program using openkal.process linked for Emscripten" + exit 1 + fi + grep -E "^(error|wasm-ld)" absent.log | head -5 + grep -q "undefined symbol: kal_process_" absent.log \ + || { cat absent.log; echo "::error::the build did not fail naming an openkal.process symbol"; exit 1; } + echo " ok the link names the symbol this platform does not have" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3429589..7848df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 0.2.2 + +- **Emscripten (`wasm32-emscripten`).** The package builds for a wasm module + over Emscripten's C library and openkal-emscripten 0.6.0, under node or in a + page. It adds `platform::emscripten`, `architecture::wasm32` and + `is_emscripten`, and selects `src/targets/emscripten.cppm`, + `src/arch/wasm32.cppm`, `src/emscripten/` and the C library's part of + `src/posix/fs.cpp`. No platform module is defined. A program that starts + another fails at link, naming the openkal symbol (`checks/absent`). + - `interrupt` reports that no request to end is delivered. Its + implementation over openkal's stop word moves to `src/kal/interrupt.cpp`, + which every other target selects. + - `machine` names `wasm32`, reports the processors Emscripten counts and + does not know the memory. + - A volume's kind is the file system Emscripten mounted there (`memfs`, + `nodefs`, …). Standard output is diverted with the C library's + descriptors, which openkal-emscripten's streams are. + - CI builds the tests that do not start programs for the target and runs them + under node (`checks/emscripten`, debug and release), along with the + compile-time constants and the R5 checks. +- **`fs::read` reads into the string, not through a 64 KiB array on the + stack.** That array was all of a wasm module's default stack (65536 bytes) + and overflowed it. +- **An enquiry asks for the kind it reads absence from.** `stamp_of` and + `identity_of` asked openkal for the size, the time or the identity, and then + read the kind. An implementation fills only the fields it is asked for + (openkal-emscripten does), so the kind stayed zero, which is + `kal_node_absent`, and an existing file was reported not found. +- **The tests ask the volume before taking a lock or renaming without + replacing,** and on a volume that does neither they check that the request is + refused and changes nothing. A test that names its scratch directory after + the program uses random bytes where no program is named. + ## 0.2.1 - **The ok-image targets take their C library, C++ run-time and loader from diff --git a/README.md b/README.md index 05707a9..d1c7e26 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ every system in CI. | Module | Contents | |---|---| -| `mcpplibs.os.platform` | `PLATFORM`, `ARCH`, `is_linux` / `is_macos` / `is_windows` / `is_posix`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | +| `mcpplibs.os.platform` | `PLATFORM`, `ARCH`, `is_linux` / `is_macos` / `is_windows` / `is_posix` / `is_emscripten`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | | `mcpplibs.os.error` | `errc` (openkal's closed set of conditions), `error`, `result` | | `mcpplibs.os.env` | variables (names without case on Windows), `NAME=value` entries as overrides (`assignments`), arguments, the program's own image, lists of directories in the host's separator (`split_list`, `join_list`), PATH search with PATHEXT | | `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `append`, `sync`, `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `owner_of`, `writable`, `locate`, `absolute`, `names_a_place`, `canonical`, a bounded `read`; what a volume does, measured (`measure`, `volume_type`), and the operations that follow it (`place`, `exchange`); names in the host's rules without the file system: `generic`, `native`, `extended`, `is_absolute`, `same`, `within` | @@ -125,6 +125,24 @@ for `-openkal`; this package declares none of them); the tests are built as images in CI and run through okrun on four systems, and `checks/constants` holds the compile-time facts of a native target. +## On Emscripten + +Built for `wasm32-emscripten`, a program over this library is a wasm module +that runs under node or in a page, over Emscripten's C library and +openkal-emscripten. `os::PLATFORM` is `platform::emscripten`, `os::ARCH` is +`architecture::wasm32`, and `os::host()` is a constant expression, as on a +native target. Files are the module's own file system (MEMFS, one supplied +directory at `/`); `os::fs::caps()` says what it does: no locks, and no rename +that refuses to replace, so `publish` takes the name exclusively and +`rename(…, false)` is refused rather than tested and then done. A program is +not started and a library is not loaded: openkal-emscripten provides no +`openkal.process`, so a program that starts one, or names itself +(`process::self`), fails at link naming the symbol (`checks/absent`), and +`library::supported()` is false. No request to end is delivered +(`interrupt::available()` is false). The machine is named `wasm32` and its +memory is not known. The tests that do not start programs are built for the +target in CI and run under node (`checks/emscripten`). + ## License Apache-2.0 diff --git a/checks/absent/mcpp.toml b/checks/absent/mcpp.toml new file mode 100644 index 0000000..c2cd23e --- /dev/null +++ b/checks/absent/mcpp.toml @@ -0,0 +1,18 @@ +# Built for Emscripten only, and expected NOT to link: openkal-emscripten +# provides no `openkal.process', so a program that names itself or starts +# another fails at link naming the openkal symbol (openkal SPEC clause 6.2's +# second time), rather than at run time with an error a caller cannot tell +# from a condition. CI asserts the failure and the name. +[package] +name = "os-check-absent" +version = "0.1.0" + +[dependencies] +os = { path = "../.." } + +# UNTIL openkal-emscripten 0.6.0 IS IN THE INDEX (mcpplibs/openkal-emscripten#8): +# the root's declaration settles the package's own `openkal-emscripten = +# "0.6.0"' (mcpp docs/05, a root `git' declaration wins and the version is +# checked against the checkout). Removed when the release is published. +[target.'cfg(os = "emscripten")'.dependencies] +openkal-emscripten = { git = "https://github.com/mcpplibs/openkal-emscripten.git", rev = "a43f7cd89fea9b9d6101cd8556d6b3f8ab7f7bcd" } diff --git a/checks/absent/src/main.cpp b/checks/absent/src/main.cpp new file mode 100644 index 0000000..a0bac12 --- /dev/null +++ b/checks/absent/src/main.cpp @@ -0,0 +1,5 @@ +// A program that names itself: openkal.process, which Emscripten does not have. +import std; +import mcpplibs.os; + +int main() { return mcpplibs::os::process::self().has_value() ? 0 : 1; } diff --git a/checks/constants/mcpp.toml b/checks/constants/mcpp.toml index 7774ced..9c46a7d 100644 --- a/checks/constants/mcpp.toml +++ b/checks/constants/mcpp.toml @@ -1,6 +1,6 @@ -# The compile-time constants of a native target (R3; part 2 E29), checked when -# the program is compiled. An ok image has neither `exe_suffix' nor a constant -# `host()', so these facts are a build of their own on the native rows rather +# The compile-time constants of a native target and of Emscripten (R3; part 2 +# E29), checked when the program is compiled. An ok image has neither +# `exe_suffix' nor a constant `host()', so these facts are a build of their own on the native rows rather # than part of tests/platform_test.cpp, which is also built as an image. [package] name = "os-check-constants" @@ -8,3 +8,10 @@ version = "0.1.0" [dependencies] os = { path = "../.." } + +# UNTIL openkal-emscripten 0.6.0 IS IN THE INDEX (mcpplibs/openkal-emscripten#8): +# the root's declaration settles the package's own `openkal-emscripten = +# "0.6.0"' (mcpp docs/05, a root `git' declaration wins and the version is +# checked against the checkout). Removed when the release is published. +[target.'cfg(os = "emscripten")'.dependencies] +openkal-emscripten = { git = "https://github.com/mcpplibs/openkal-emscripten.git", rev = "a43f7cd89fea9b9d6101cd8556d6b3f8ab7f7bcd" } diff --git a/checks/constants/src/main.cpp b/checks/constants/src/main.cpp index c358e41..402b556 100644 --- a/checks/constants/src/main.cpp +++ b/checks/constants/src/main.cpp @@ -1,11 +1,12 @@ -// Built on every native row: each fact is a constant expression there. +// Built on every native row and for Emscripten: each fact is a constant +// expression there. import std; import mcpplibs.os; namespace os = mcpplibs::os; static_assert(os::host() == os::PLATFORM); -static_assert(os::is_linux + os::is_macos + os::is_windows == 1); +static_assert(os::is_linux + os::is_macos + os::is_windows + os::is_emscripten == 1); static_assert(!os::is_image); static_assert(os::exe_suffix == (os::is_windows ? ".exe" : "")); static_assert(os::path_list_separator == (os::is_windows ? ';' : ':')); diff --git a/checks/emscripten/mcpp.toml b/checks/emscripten/mcpp.toml new file mode 100644 index 0000000..52dede3 --- /dev/null +++ b/checks/emscripten/mcpp.toml @@ -0,0 +1,20 @@ +# The package's tests built for Emscripten (`wasm32-emscripten') and run under +# node: a wasm module over Emscripten's C library and openkal-emscripten. +# +# Every test of the package has its counterpart here except the two that start +# programs (process_test, ipc_test): openkal-emscripten provides no +# `openkal.process', so a program that starts one fails at link naming the +# symbol, which CI asserts instead (absent/). +[package] +name = "os-check-emscripten" +version = "0.1.0" + +[dependencies] +os = { path = "../.." } + +# UNTIL openkal-emscripten 0.6.0 IS IN THE INDEX (mcpplibs/openkal-emscripten#8): +# the root's declaration settles the package's own `openkal-emscripten = +# "0.6.0"' (mcpp docs/05, a root `git' declaration wins and the version is +# checked against the checkout). Removed when the release is published. +[target.'cfg(os = "emscripten")'.dependencies] +openkal-emscripten = { git = "https://github.com/mcpplibs/openkal-emscripten.git", rev = "a43f7cd89fea9b9d6101cd8556d6b3f8ab7f7bcd" } diff --git a/checks/emscripten/tests/dirs_terminal_test.cpp b/checks/emscripten/tests/dirs_terminal_test.cpp new file mode 100644 index 0000000..da9259e --- /dev/null +++ b/checks/emscripten/tests/dirs_terminal_test.cpp @@ -0,0 +1,6 @@ +// dirs_terminal_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated +// because the dependency scan reads this file and not the one it includes. +#include "../../../tests/check.hpp" +import std; +import mcpplibs.os; +#include "../../../tests/dirs_terminal_test.cpp" diff --git a/checks/emscripten/tests/fs_test.cpp b/checks/emscripten/tests/fs_test.cpp new file mode 100644 index 0000000..c982d02 --- /dev/null +++ b/checks/emscripten/tests/fs_test.cpp @@ -0,0 +1,6 @@ +// fs_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated +// because the dependency scan reads this file and not the one it includes. +#include "../../../tests/check.hpp" +import std; +import mcpplibs.os; +#include "../../../tests/fs_test.cpp" diff --git a/checks/emscripten/tests/library_test.cpp b/checks/emscripten/tests/library_test.cpp new file mode 100644 index 0000000..077a116 --- /dev/null +++ b/checks/emscripten/tests/library_test.cpp @@ -0,0 +1,6 @@ +// library_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated +// because the dependency scan reads this file and not the one it includes. +#include "../../../tests/check.hpp" +import std; +import mcpplibs.os; +#include "../../../tests/library_test.cpp" diff --git a/checks/emscripten/tests/linux_test.cpp b/checks/emscripten/tests/linux_test.cpp new file mode 100644 index 0000000..fe2d15a --- /dev/null +++ b/checks/emscripten/tests/linux_test.cpp @@ -0,0 +1,6 @@ +// linux_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated +// because the dependency scan reads this file and not the one it includes. +#include "../../../tests/check.hpp" +import std; +import mcpplibs.os; +#include "../../../tests/linux_test.cpp" diff --git a/checks/emscripten/tests/platform_test.cpp b/checks/emscripten/tests/platform_test.cpp new file mode 100644 index 0000000..a9a789d --- /dev/null +++ b/checks/emscripten/tests/platform_test.cpp @@ -0,0 +1,6 @@ +// platform_test.cpp of mcpplibs.os, compiled for Emscripten. The imports are restated +// because the dependency scan reads this file and not the one it includes. +#include "../../../tests/check.hpp" +import std; +import mcpplibs.os; +#include "../../../tests/platform_test.cpp" diff --git a/mcpp.toml b/mcpp.toml index 0dd8445..8e94566 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.2.1" +version = "0.2.2" description = "mcpplibs.os: a C++ operating-system library over openkal --- processes, files, environment, terminals, directories and local IPC, with compile-time platform constants and a few platform modules." license = "Apache-2.0" authors = ["mcpplibs"] @@ -23,6 +23,13 @@ openkal-macos = "0.15.1" [target.windows.dependencies] openkal-windows = "0.13.3" +# Emscripten (`wasm32-emscripten`): openkal written above the C library, with +# twelve of openkal's fifteen interfaces. `openkal.process', `openkal.exec' and +# `openkal.space' are absent, so a program that starts one fails at link +# naming the symbol; `openkal.task' is present in a `-pthread' link only. +[target.'cfg(os = "emscripten")'.dependencies] +openkal-emscripten = "0.6.0" + [targets.os] kind = "lib" @@ -34,6 +41,12 @@ kind = "lib" [build] sources = ["src/*.cppm", "src/*.cpp"] +# A request to end, over openkal's stop word and its wait: on every target that +# has `openkal.process' and `openkal.task'. Emscripten has neither, and its +# interrupt is src/emscripten/interrupt.cpp. +[target.'cfg(not(os = "emscripten"))'.build] +sources = ["src/kal/interrupt.cpp"] + # The architecture of the build, as a constant beside the platform's. [target.'cfg(arch = "x86_64")'.build] sources = ["src/arch/x86_64.cppm"] @@ -44,6 +57,9 @@ sources = ["src/arch/aarch64.cppm"] [target.'cfg(arch = "riscv64")'.build] sources = ["src/arch/riscv64.cppm"] +[target.'cfg(arch = "wasm32")'.build] +sources = ["src/arch/wasm32.cppm"] + [target.linux.build] sources = ["src/targets/linux.cppm", "src/linux/*.cpp", "src/posix/*.cpp"] @@ -84,6 +100,13 @@ sources = ["src/windows/crt/stdio.cpp"] [target.'cfg(all(windows, c-abi = "musl"))'.build] sources = ["src/windows/posix/stdio.cpp"] +# EMSCRIPTEN: a wasm module over Emscripten's C library, under node or in a +# page. No platform module is defined; the file system's durability and +# ownership are the C library's (src/posix/fs.cpp), and the rest is +# src/emscripten/. +[target.'cfg(os = "emscripten")'.build] +sources = ["src/targets/emscripten.cppm", "src/emscripten/*.cpp", "src/posix/fs.cpp"] + # AN OK IMAGE (`-openkal', openkal SPEC clause 12): no implementation # beneath --- the loader binds the image's `kal_*' --- and no platform module # defined. `host()' and the conventions are answered when the image is loaded. diff --git a/src/arch/aarch64.cppm b/src/arch/aarch64.cppm index 5f23e09..420e12b 100644 --- a/src/arch/aarch64.cppm +++ b/src/arch/aarch64.cppm @@ -4,7 +4,7 @@ export module mcpplibs.os.platform:arch; export namespace mcpplibs::os { -enum class architecture { x86_64, aarch64, riscv64 }; +enum class architecture { x86_64, aarch64, riscv64, wasm32 }; // What the program is built for. The machine it runs upon may differ (a // translated program); `machine::arch()' answers that. diff --git a/src/arch/riscv64.cppm b/src/arch/riscv64.cppm index 3881dcb..009950c 100644 --- a/src/arch/riscv64.cppm +++ b/src/arch/riscv64.cppm @@ -4,7 +4,7 @@ export module mcpplibs.os.platform:arch; export namespace mcpplibs::os { -enum class architecture { x86_64, aarch64, riscv64 }; +enum class architecture { x86_64, aarch64, riscv64, wasm32 }; // What the program is built for. The machine it runs upon may differ (a // translated program); `machine::arch()' answers that. diff --git a/src/arch/wasm32.cppm b/src/arch/wasm32.cppm new file mode 100644 index 0000000..58f9e0d --- /dev/null +++ b/src/arch/wasm32.cppm @@ -0,0 +1,13 @@ +// The architecture of this build: wasm32. Selected by the manifest's +// [target.'cfg(arch = "wasm32")'.build]. +export module mcpplibs.os.platform:arch; + +export namespace mcpplibs::os { + +enum class architecture { x86_64, aarch64, riscv64, wasm32 }; + +// What the program is built for. A wasm module runs upon whatever machine its +// host runs upon, and is told nothing of it; `machine::arch()' answers wasm32. +inline constexpr architecture ARCH = architecture::wasm32; + +} // namespace mcpplibs::os diff --git a/src/arch/x86_64.cppm b/src/arch/x86_64.cppm index d9ce4ec..cc993dc 100644 --- a/src/arch/x86_64.cppm +++ b/src/arch/x86_64.cppm @@ -4,7 +4,7 @@ export module mcpplibs.os.platform:arch; export namespace mcpplibs::os { -enum class architecture { x86_64, aarch64, riscv64 }; +enum class architecture { x86_64, aarch64, riscv64, wasm32 }; // What the program is built for. The machine it runs upon may differ (a // translated program); `machine::arch()' answers that. diff --git a/src/emscripten/exec.cpp b/src/emscripten/exec.cpp new file mode 100644 index 0000000..2738cf3 --- /dev/null +++ b/src/emscripten/exec.cpp @@ -0,0 +1,14 @@ +// Emscripten starts no program and replaces no program's image. +module mcpplibs.os.process; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::process::sys { + +result replace_image(const std::filesystem::path&, const std::vector&, + const std::vector&, const std::filesystem::path&) { + return std::unexpected(error{ errc::not_supported, "this system replaces no program's image" }); +} + +} // namespace mcpplibs::os::process::sys diff --git a/src/emscripten/fs.cpp b/src/emscripten/fs.cpp new file mode 100644 index 0000000..4a1d7a6 --- /dev/null +++ b/src/emscripten/fs.cpp @@ -0,0 +1,46 @@ +// Emscripten's own of mcpplibs.os.fs: nothing is exchanged (the C library has +// no renameat2), and the kind of a volume is the file system Emscripten +// mounted there, asked of its FS (statfs answers the same constants for every +// one). Durability, ownership and whether a node may be written are the C +// library's, in src/posix/fs.cpp. +module; +#include + +module mcpplibs.os.fs; + +import std; + +namespace mcpplibs::os::fs::sys { + +int exchange(const std::filesystem::path&, const std::filesystem::path&) { return -1; } +bool exchange_unsupported(int c) { return c == -1; } + +// `memfs', `nodefs', `idbfs', `workerfs', `noderawfs' or `opfs', by the mount +// the directory lies in; nothing where the FS cannot say (a name not found, +// or a file system this list does not name). +std::string volume_type(const std::filesystem::path& directory) { + const std::string name = directory.generic_string(); + const int kind = EM_ASM_INT({ + try { + var t = FS.lookupPath(UTF8ToString($0), { follow: true }).node.mount.type; + if (typeof MEMFS !== 'undefined' && t === MEMFS) return 1; + if (typeof NODEFS !== 'undefined' && t === NODEFS) return 2; + if (typeof IDBFS !== 'undefined' && t === IDBFS) return 3; + if (typeof WORKERFS !== 'undefined' && t === WORKERFS) return 4; + if (typeof NODERAWFS !== 'undefined' && t === NODERAWFS) return 5; + if (typeof OPFS !== 'undefined' && t === OPFS) return 6; + } catch (e) {} + return 0; + }, name.c_str()); + switch (kind) { + case 1: return "memfs"; + case 2: return "nodefs"; + case 3: return "idbfs"; + case 4: return "workerfs"; + case 5: return "noderawfs"; + case 6: return "opfs"; + } + return {}; +} + +} // namespace mcpplibs::os::fs::sys diff --git a/src/emscripten/interrupt.cpp b/src/emscripten/interrupt.cpp new file mode 100644 index 0000000..f062ef8 --- /dev/null +++ b/src/emscripten/interrupt.cpp @@ -0,0 +1,28 @@ +// mcpplibs.os.interrupt on Emscripten: no request to end reaches a wasm +// module as a notice (openkal-emscripten provides no `openkal.process', whose +// stop word it would be), so none is available and none arrives; and no +// signal ends a program whose reader is gone: its writes are refused. +module mcpplibs.os.interrupt; + +import std; + +namespace mcpplibs::os::interrupt { + +bool available() { return false; } + +bool requested() { return false; } + +bool wait(std::optional timeout) { + if (timeout) std::this_thread::sleep_for(*timeout); + return false; +} + +void end_as_requested() {} + +void end_as_output_closed() { sys::end_as_output_closed(); } + +} // namespace mcpplibs::os::interrupt + +namespace mcpplibs::os::interrupt::sys { +void end_as_output_closed() {} +} // namespace mcpplibs::os::interrupt::sys diff --git a/src/emscripten/library.cpp b/src/emscripten/library.cpp new file mode 100644 index 0000000..b5079ce --- /dev/null +++ b/src/emscripten/library.cpp @@ -0,0 +1,34 @@ +// mcpplibs.os.library on Emscripten: a module loads another only when the +// program is linked as a main module (`-sMAIN_MODULE'), which is the +// program's decision at link and not this package's; linked as an ordinary +// program, as mcpp links one, there is nothing to open. +module mcpplibs.os.library; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::library { + +handle& handle::operator=(handle&& other) noexcept { + native_ = std::exchange(other.native_, nullptr); + owned_ = std::exchange(other.owned_, false); + return *this; +} + +handle::~handle() = default; + +void* handle::symbol(std::string_view) const { return nullptr; } + +result open(const std::filesystem::path&) { + return std::unexpected(error{errc::not_supported, "loading a library in a wasm module", 0}); +} + +std::optional loaded(std::string_view) { return std::nullopt; } + +void* process_symbol(std::string_view) { return nullptr; } + +bool supported() noexcept { return false; } + +std::string_view suffix() noexcept { return ""; } + +} // namespace mcpplibs::os::library diff --git a/src/emscripten/machine.cpp b/src/emscripten/machine.cpp new file mode 100644 index 0000000..8ee67ae --- /dev/null +++ b/src/emscripten/machine.cpp @@ -0,0 +1,33 @@ +// mcpplibs.os.machine on Emscripten: a wasm module is told nothing of the +// machine beneath its host but the number of processors, as Emscripten's C +// library reports it: in a `-pthread' link the host's (`navigator. +// hardwareConcurrency' in a page, the operating system's count under node), +// and one without, where the module has one context to run in. +module; +#include + +module mcpplibs.os.machine; + +import std; +import mcpplibs.os.platform; + +namespace mcpplibs::os::machine { + +// The architecture the module was built for, which is the one it runs upon: +// the host translates it, and says nothing of its own. +std::string arch() { return std::string(name(ARCH)); } + +// Not known: the module's heap limit is not the machine's memory, and an +// answer guessed here would be wrong somewhere. +std::optional memory() { return std::nullopt; } + +std::optional memory_available() { return std::nullopt; } + +processor_count processors() { + processor_count c; + const int n = ::emscripten_num_logical_cores(); + c.logical = c.physical = n > 0 ? static_cast(n) : 1u; + return c; +} + +} // namespace mcpplibs::os::machine diff --git a/src/emscripten/stdio.cpp b/src/emscripten/stdio.cpp new file mode 100644 index 0000000..3db221d --- /dev/null +++ b/src/emscripten/stdio.cpp @@ -0,0 +1,41 @@ +// The output diverted where the streams are: Emscripten's C library keeps the +// descriptors openkal-emscripten's standard streams borrow, so its `dup' and +// `dup2' move what both write to. +module; +#include +#include +#include + +module mcpplibs.os.stdio; + +import std; + +namespace mcpplibs::os::stdio::sys { + +int divert_output() { + const int saved = ::fcntl(1, F_DUPFD_CLOEXEC, 3); + if (saved < 0) return -1; + if (::dup2(2, 1) < 0) { + ::close(saved); + return -1; + } + return saved; +} + +void restore_output(int saved) { + ::dup2(saved, 1); + ::close(saved); +} + +bool write_descriptor(int descriptor, std::string_view bytes) { + std::size_t done = 0; + while (done < bytes.size()) { + const auto n = ::write(descriptor, bytes.data() + done, bytes.size() - done); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + done += static_cast(n); + } + return true; +} + +} // namespace mcpplibs::os::stdio::sys diff --git a/src/emscripten/terminal.cpp b/src/emscripten/terminal.cpp new file mode 100644 index 0000000..cc98f57 --- /dev/null +++ b/src/emscripten/terminal.cpp @@ -0,0 +1,7 @@ +// A host's terminal has no process groups to be outside of: a module is in +// its foreground whenever it runs. +module mcpplibs.os.terminal; + +namespace mcpplibs::os::terminal::sys { +bool foreground() { return true; } +} // namespace mcpplibs::os::terminal::sys diff --git a/src/fs.cpp b/src/fs.cpp index 7cb6cb1..f20dbe5 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -250,15 +250,23 @@ result read_up_to(const std::filesystem::path& file, std::optional< const int rc = kal_fs_open(dir_of(*l), l->relative.data(), l->relative.size(), kal::fs::open::read.bits, &f.f); if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("reading {}", generic_utf8(file)))); + // Read into the string's own storage, not through a buffer on the stack: + // a 64 KiB array is all of a wasm module's default stack (Emscripten's + // 65536 bytes, where `read' overflowed it) and a large part of a small + // thread's. + constexpr std::size_t chunk = 65536; std::string out; - char buf[65536]; for (;;) { - std::size_t want = sizeof buf; + std::size_t want = chunk; if (at_most) want = std::min(want, *at_most + 1 - out.size()); - const kal_intptr n = kal_stream_read(kal_fs_stream(f.f), buf, want); + const std::size_t at = out.size(); + kal_intptr n = 0; + out.resize_and_overwrite(at + want, [&](char* p, std::size_t) { + n = kal_stream_read(kal_fs_stream(f.f), p + at, want); + return at + (n > 0 ? static_cast(n) : 0); + }); if (n < 0) return std::unexpected(from_kal(static_cast(-n), std::format("reading {}", generic_utf8(file)))); if (n == 0) break; - out.append(buf, static_cast(n)); if (at_most && out.size() > *at_most) return std::unexpected(error{ errc::too_large, std::format("reading {} (more than {} bytes)", generic_utf8(file), *at_most) }); } @@ -452,7 +460,11 @@ result info(const std::filesystem::path& node, kal_u32 wanted, st if (!l) return std::unexpected(l.error()); kal_node_info i{}; i.self_size = sizeof i; - const int rc = kal_fs_info(dir_of(*l), l->relative.data(), l->relative.size(), 0, wanted, &i); + // The kind is asked for as well, because absence is read from it: an + // implementation fills only the fields it was asked for (openkal-emscripten + // does), and a kind left at zero reads as kal_node_absent. + const int rc = kal_fs_info(dir_of(*l), l->relative.data(), l->relative.size(), 0, + wanted | kal::fs::field::kind, &i); if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("{} {}", doing, generic_utf8(node)))); if (i.kind == kal_node_absent) return std::unexpected(error{ errc::not_found, std::format("{} {}", doing, generic_utf8(node)) }); return i; diff --git a/src/interrupt.cpp b/src/kal/interrupt.cpp similarity index 89% rename from src/interrupt.cpp rename to src/kal/interrupt.cpp index 27efbb5..3a03319 100644 --- a/src/interrupt.cpp +++ b/src/kal/interrupt.cpp @@ -1,3 +1,6 @@ +// mcpplibs.os.interrupt over openkal's stop word (`openkal.process') and its +// wait (`openkal.task'): every target but Emscripten, which has neither +// interface (src/emscripten/interrupt.cpp). module mcpplibs.os.interrupt; import std; diff --git a/src/platform.cppm b/src/platform.cppm index d5de46a..e7627e3 100644 --- a/src/platform.cppm +++ b/src/platform.cppm @@ -27,6 +27,10 @@ inline constexpr bool is_macos = PLATFORM == platform::macos; inline constexpr bool is_windows = PLATFORM == platform::windows; inline constexpr bool is_posix = is_linux || is_macos; inline constexpr bool is_image = PLATFORM == platform::openkal; +// A wasm module over Emscripten's C library (0.2.2): no program is started and +// no library loaded, the file system is the module's own (MEMFS), and the +// streams are the host's. +inline constexpr bool is_emscripten = PLATFORM == platform::emscripten; // The name of a platform, as mcpp's target vocabulary and openkal's // `kal_env_system' spell it. @@ -36,6 +40,7 @@ constexpr std::string_view name(platform p) noexcept { case platform::macos: return "macos"; case platform::windows: return "windows"; case platform::openkal: return "openkal"; + case platform::emscripten: return "emscripten"; } return "unknown"; } @@ -45,6 +50,7 @@ constexpr std::string_view name(architecture a) noexcept { case architecture::x86_64: return "x86_64"; case architecture::aarch64: return "aarch64"; case architecture::riscv64: return "riscv64"; + case architecture::wasm32: return "wasm32"; } return "unknown"; } diff --git a/src/posix/fs.cpp b/src/posix/fs.cpp index 7f9128e..1f397b4 100644 --- a/src/posix/fs.cpp +++ b/src/posix/fs.cpp @@ -1,5 +1,5 @@ -// What Linux and macOS share of mcpplibs.os.fs's own: durability, ownership, -// whether a node may be written. +// What Linux, macOS and Emscripten share of mcpplibs.os.fs's own, from the C +// library: durability, ownership, whether a node may be written. module; #include #include diff --git a/src/targets/emscripten.cppm b/src/targets/emscripten.cppm new file mode 100644 index 0000000..d760774 --- /dev/null +++ b/src/targets/emscripten.cppm @@ -0,0 +1,21 @@ +// The target of this build: emscripten, a C library over a JavaScript host +// (node, or a page). Selected by the manifest's [target.'cfg(os = "emscripten")'.build]. +export module mcpplibs.os.platform:target; + +import std; + +export namespace mcpplibs::os { + +enum class platform { linux, macos, windows, openkal, emscripten }; + +// What the program is built for. +inline constexpr platform PLATFORM = platform::emscripten; + +// What the program runs upon: the same module runs under node and in a page, +// and the conventions it follows are its C library's and MEMFS's in both. +constexpr platform host() noexcept { return PLATFORM; } + +inline constexpr std::string_view exe_suffix = ""; +inline constexpr char path_list_separator = ':'; + +} // namespace mcpplibs::os diff --git a/src/targets/linux.cppm b/src/targets/linux.cppm index 1eb70be..b10b330 100644 --- a/src/targets/linux.cppm +++ b/src/targets/linux.cppm @@ -5,7 +5,7 @@ import std; export namespace mcpplibs::os { -enum class platform { linux, macos, windows, openkal }; +enum class platform { linux, macos, windows, openkal, emscripten }; // What the program is built for. inline constexpr platform PLATFORM = platform::linux; diff --git a/src/targets/macos.cppm b/src/targets/macos.cppm index 5c5428f..bab1007 100644 --- a/src/targets/macos.cppm +++ b/src/targets/macos.cppm @@ -5,7 +5,7 @@ import std; export namespace mcpplibs::os { -enum class platform { linux, macos, windows, openkal }; +enum class platform { linux, macos, windows, openkal, emscripten }; // What the program is built for. inline constexpr platform PLATFORM = platform::macos; diff --git a/src/targets/openkal.cppm b/src/targets/openkal.cppm index 23319b9..97e1b46 100644 --- a/src/targets/openkal.cppm +++ b/src/targets/openkal.cppm @@ -6,7 +6,7 @@ import std; export namespace mcpplibs::os { -enum class platform { linux, macos, windows, openkal }; +enum class platform { linux, macos, windows, openkal, emscripten }; inline constexpr platform PLATFORM = platform::openkal; diff --git a/src/targets/windows.cppm b/src/targets/windows.cppm index af7b0e1..064422f 100644 --- a/src/targets/windows.cppm +++ b/src/targets/windows.cppm @@ -5,7 +5,7 @@ import std; export namespace mcpplibs::os { -enum class platform { linux, macos, windows, openkal }; +enum class platform { linux, macos, windows, openkal, emscripten }; // What the program is built for. inline constexpr platform PLATFORM = platform::windows; diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index e6f9fd8..4291e1d 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -6,8 +6,16 @@ import mcpplibs.os; namespace os = mcpplibs::os; namespace stdfs = std::filesystem; +// A name no other run of this test uses at once: this program's identifier, +// or on Emscripten, which names no program (no `openkal.process'), random +// bytes; the module's file system is its own there. +static std::string run_tag() { + if constexpr (os::is_emscripten) return os::random::hex(8).value_or("0"); + else return std::to_string(os::process::self()->id); +} + int main() { - const auto dir = stdfs::temp_directory_path() / std::format("mcpplibs-os-fs-{}", os::process::self()->id); + const auto dir = stdfs::temp_directory_path() / std::format("mcpplibs-os-fs-{}", run_tag()); stdfs::remove_all(dir); stdfs::create_directories(dir); @@ -53,12 +61,17 @@ int main() { check(!second && second.error().code == os::errc::exists, "a published name is not replaced"); check(os::fs::read(pub).value_or("") == "first", "and keeps what was published first"); - { + // Locks where the volume has them (every native volume here); a volume + // without them (MEMFS, Emscripten) refuses rather than pretending. + if (!caps || caps->locks) { auto a = os::fs::lock::acquire(dir / "lock", os::fs::lock::kind::exclusive, false); check(a.has_value() && a->held(), "an exclusive lock is taken"); + } else { + auto a = os::fs::lock::acquire(dir / "lock", os::fs::lock::kind::exclusive, false); + check(!a && a.error().code == os::errc::not_supported, "a volume without locks refuses one"); } auto again = os::fs::lock::acquire(dir / "lock", os::fs::lock::kind::exclusive, false); - check(again.has_value(), "and is taken again once released"); + check(again.has_value() || (caps && !caps->locks), "and is taken again once released"); // A lock held for the rest of the program: the directory is removed below // while it is held, where the volume removes a held name at once. @@ -78,7 +91,15 @@ int main() { check(stdfs::exists(dir / "link" / "."), "and reaches the directory"); } } - check(os::fs::rename(file, dir / "b.txt", false).has_value(), "a rename that does not replace renames to a free name"); + if (!caps || caps->rename_no_replace) { + check(os::fs::rename(file, dir / "b.txt", false).has_value(), "a rename that does not replace renames to a free name"); + } else { + // A volume that cannot test and rename as one act refuses, and renames nothing. + const auto r = os::fs::rename(file, dir / "b.txt", false); + check(!r && r.error().code == os::errc::not_supported && stdfs::exists(file) && !stdfs::exists(dir / "b.txt"), + "a rename that does not replace is refused where the volume cannot, and renames nothing"); + check(os::fs::rename(file, dir / "b.txt", true).has_value(), "and a rename that replaces renames"); + } check(os::fs::names_a_place("bin/tool") && !os::fs::names_a_place("tool"), "a name with a separator says where a file is, a bare one does not"); @@ -127,7 +148,7 @@ int main() { check(stdfs::exists(f), "and the system sees it under that name"); check(os::fs::write_atomic(f, "again").has_value() && os::fs::read(f).value_or("") == "again", "replaced all at once"); const auto moved = sub / os::fs::from_utf8("\xE6\x96\xB0 " + name); - check(os::fs::rename(f, moved, false).has_value() && stdfs::exists(moved), "and renamed"); + check(os::fs::rename(f, moved, caps && !caps->rename_no_replace).has_value() && stdfs::exists(moved), "and renamed"); const auto missing = os::fs::read(sub / os::fs::from_utf8("\xE6\xB2\xA1\xE6\x9C\x89")); check(!missing && missing.error().message().contains("\xE6\xB2\xA1\xE6\x9C\x89"), "an error names the file in UTF-8"); check(os::fs::generic_utf8(os::fs::absolute(sub)).ends_with(name), "absolute keeps the name"); @@ -144,7 +165,7 @@ int main() { { // 0.2.0: appending, durability, measured volumes, placing, exchanging. const auto dir = std::filesystem::temp_directory_path() - / std::format("mcpplibs-os-fs2-{}", os::process::self()->id); + / std::format("mcpplibs-os-fs2-{}", run_tag()); std::filesystem::create_directories(dir); check(os::fs::append(dir / "log", "a").has_value() && os::fs::append(dir / "log", "b").has_value() && os::fs::read(dir / "log").value_or("") == "ab", diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp index 51dccaa..8739e5c 100644 --- a/tests/platform_test.cpp +++ b/tests/platform_test.cpp @@ -8,9 +8,11 @@ import mcpplibs.os; namespace os = mcpplibs::os; -static_assert(os::is_linux + os::is_macos + os::is_windows + os::is_image == 1); +static_assert(os::is_linux + os::is_macos + os::is_windows + os::is_image + os::is_emscripten == 1); static_assert(os::is_posix == (os::is_linux || os::is_macos)); -static_assert(os::name(os::ARCH) == "x86_64" || os::name(os::ARCH) == "aarch64" || os::name(os::ARCH) == "riscv64"); +static_assert(os::name(os::ARCH) == "x86_64" || os::name(os::ARCH) == "aarch64" || os::name(os::ARCH) == "riscv64" + || os::name(os::ARCH) == "wasm32"); +static_assert(os::is_emscripten == (os::name(os::ARCH) == "wasm32")); int main() { const auto host = os::host(); @@ -27,14 +29,21 @@ int main() { // library answers the program's other questions. if constexpr (os::is_linux) check(os::machine::arch() == os::name(os::ARCH), "Linux: the machine is the processor the program runs on"); - if constexpr (!os::is_image) + // A wasm module is told nothing of the machine beneath its host but the + // number of processors, and it runs the instructions it was built for. + if constexpr (os::is_emscripten) { + check(os::machine::arch() == "wasm32", "Emscripten: the machine is the module's"); + check(!os::machine::memory() && !os::machine::memory_available(), "Emscripten: the machine's memory is not known"); + } + if constexpr (!os::is_image && !os::is_emscripten) check(os::machine::memory().value_or(0) > 0, "the machine's memory is known"); const auto a = os::random::hex(16), b = os::random::hex(16); check(a && a->size() == 32 && b && *a != *b, "the system's source gives bytes that differ from call to call"); { const auto p = os::machine::processors(); check(p.logical >= 1 && p.physical >= 1 && p.physical <= p.logical, "processors: logical and physical counted"); - if (!os::is_image) check(os::machine::memory_available().value_or(0) > 0, "and the memory available now"); + if (!os::is_image && !os::is_emscripten) + check(os::machine::memory_available().value_or(0) > 0, "and the memory available now"); } if constexpr (os::is_windows) { check(os::windows::code_page() != 0, "Windows: the ANSI code page is known");