From 060c0f241ace2005bdc8e90bb4b03f7cc5e4fb56 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 02:02:15 +0900 Subject: [PATCH 1/8] the units beside openkal reach each system through its own interface A C library on openkal (openkal-musl over openkal-, on the system's own triple) presents POSIX and openkal rather than the system. The per-system units now reach the system without it: - Linux: the kernel's calls, one trap per architecture (src/linux/kernel.h, src/linux/arch/), for uname, sysinfo, statfs and renameat2. Under such a C library uname named openkal, sysinfo was absent and renameat2's RENAME_EXCHANGE was a plain rename. - macOS: the kernel's statfs64 and renameatx_np (src/macos/kernel.h), and the libSystem names no C library defines for sysctl and the host's statistics (src/macos/darwin.h), declared rather than included and allowed undefined (-U) where the link's libSystem is openkal-macos's stub. - Windows: DWORD as 32 bits and WCHAR as char16_t in win32.h under every C environment, names converted from UTF-8 (text.h); kernel32's names beyond the ones openkal-windows calls are found when first called. - The process's output diversion, SIGPIPE's reset before exec and the ending for a closed reader use the kernel's descriptors and dispositions on Linux and macOS (src/posix/kernel.h); on Windows the diversion is the C runtime's (crt/stdio.cpp) or, for a C library on openkal, that library's descriptor and the process's standard handle (posix/stdio.cpp), selected by c-abi. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mcpp.toml | 33 +++++++- src/linux/arch/aarch64.cpp | 85 +++++++++++++++++++ src/linux/arch/riscv64.cpp | 85 +++++++++++++++++++ src/linux/arch/x86_64.cpp | 85 +++++++++++++++++++ src/linux/fs.cpp | 26 +++--- src/linux/kernel.h | 45 ++++++++++ src/linux/machine.cpp | 140 +++++++++++++++++++------------- src/macos/arch/aarch64.cpp | 72 ++++++++++++++++ src/macos/arch/x86_64.cpp | 69 ++++++++++++++++ src/macos/darwin.h | 37 +++++++++ src/macos/fs.cpp | 45 +++++++--- src/macos/kernel.h | 30 +++++++ src/macos/machine.cpp | 27 +++--- src/macos/macos.cpp | 4 +- src/posix/exec.cpp | 9 +- src/posix/interrupt.cpp | 16 ++-- src/posix/kernel.h | 33 ++++++++ src/posix/stdio.cpp | 25 +++--- src/windows/{ => crt}/stdio.cpp | 19 ++++- src/windows/fs.cpp | 18 ++-- src/windows/library.cpp | 23 ++---- src/windows/machine.cpp | 12 +-- src/windows/posix/stdio.cpp | 65 +++++++++++++++ src/windows/text.h | 48 +++++++++++ src/windows/win32.h | 126 +++++++++++++++++++++------- src/windows/windows.cpp | 58 +++++++------ 26 files changed, 1025 insertions(+), 210 deletions(-) create mode 100644 src/linux/arch/aarch64.cpp create mode 100644 src/linux/arch/riscv64.cpp create mode 100644 src/linux/arch/x86_64.cpp create mode 100644 src/linux/kernel.h create mode 100644 src/macos/arch/aarch64.cpp create mode 100644 src/macos/arch/x86_64.cpp create mode 100644 src/macos/darwin.h create mode 100644 src/macos/kernel.h create mode 100644 src/posix/kernel.h rename src/windows/{ => crt}/stdio.cpp (57%) create mode 100644 src/windows/posix/stdio.cpp create mode 100644 src/windows/text.h diff --git a/mcpp.toml b/mcpp.toml index 1da8b06..71d7f3c 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.2.0" +version = "0.2.1" 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"] @@ -47,12 +47,43 @@ sources = ["src/arch/riscv64.cppm"] [target.linux.build] sources = ["src/targets/linux.cppm", "src/linux/*.cpp", "src/posix/*.cpp"] +# The kernel's calls (src/linux/kernel.h): the trap of each architecture. +[target.'cfg(all(linux, arch = "x86_64"))'.build] +sources = ["src/linux/arch/x86_64.cpp"] + +[target.'cfg(all(linux, arch = "aarch64"))'.build] +sources = ["src/linux/arch/aarch64.cpp"] + +[target.'cfg(all(linux, arch = "riscv64"))'.build] +sources = ["src/linux/arch/riscv64.cpp"] + [target.macos.build] sources = ["src/targets/macos.cppm", "src/macos/*.cpp", "src/posix/*.cpp"] +# libSystem's names the macOS units call (src/macos/darwin.h), allowed to be +# absent from a link whose libSystem is openkal-macos's stub: the loader finds +# them in libSystem. A link against the SDK binds them as usual. +ldflags = ["-Wl,-U,_sysctlbyname", "-Wl,-U,_mach_host_self", "-Wl,-U,_host_page_size", + "-Wl,-U,_host_statistics64"] + +[target.'cfg(all(macos, arch = "x86_64"))'.build] +sources = ["src/macos/arch/x86_64.cpp"] + +[target.'cfg(all(macos, arch = "aarch64"))'.build] +sources = ["src/macos/arch/aarch64.cpp"] [target.windows.build] sources = ["src/targets/windows.cppm", "src/windows/*.cpp"] +# The standard output is diverted where the C library keeps it: the Microsoft +# C runtime's descriptors, which set the process's standard handles too; or, +# for a C library on openkal (openkal-musl), that library's descriptors and the +# process's standard handles, each kept apart. +[target.'cfg(all(windows, not(c-abi = "musl")))'.build] +sources = ["src/windows/crt/stdio.cpp"] + +[target.'cfg(all(windows, c-abi = "musl"))'.build] +sources = ["src/windows/posix/stdio.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/linux/arch/aarch64.cpp b/src/linux/arch/aarch64.cpp new file mode 100644 index 0000000..c633d96 --- /dev/null +++ b/src/linux/arch/aarch64.cpp @@ -0,0 +1,85 @@ +// The kernel's calls on aarch64 Linux (src/posix/kernel.h, src/linux/kernel.h): +// the trap, and the numbers of the calls this package makes, from the kernel's +// include/uapi/asm-generic/unistd.h. +#include "../kernel.h" +#include "../../posix/kernel.h" + +namespace { + +long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { + register long x8 __asm__("x8") = n; + register long x0 __asm__("x0") = a; + register long x1 __asm__("x1") = b; + register long x2 __asm__("x2") = c; + register long x3 __asm__("x3") = d; + register long x4 __asm__("x4") = e; + __asm__ __volatile__("svc #0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) : "memory", "cc"); + return x0; +} + +constexpr long nr_write = 64, + nr_close = 57, + nr_fcntl = 25, + nr_dup3 = 24, + nr_rt_sigaction = 134, + nr_rt_sigprocmask = 135, + nr_getpid = 172, + nr_kill = 129, + nr_uname = 160, + nr_sysinfo = 179, + nr_statfs = 43, + nr_renameat2 = 276; + +constexpr long f_dupfd_cloexec = 1030, sig_unblock = 1; + +} // namespace + +namespace mcpplibs::os::kernel { + +long dup_above(int fd, int lowest) { return call(nr_fcntl, fd, f_dupfd_cloexec, lowest); } + +long dup_onto(int from, int to) { + if (from == to) return call(nr_fcntl, from, 1 /* F_GETFD */) < 0 ? -9 /* EBADF */ : to; + return call(nr_dup3, from, to, 0); +} + +long write(int fd, const void* bytes, unsigned long count) { + return call(nr_write, fd, reinterpret_cast(bytes), static_cast(count)); +} + +long close(int fd) { return call(nr_close, fd); } + +void default_signal(int signal) { + // The kernel's `struct sigaction', zero: SIG_DFL, no flags, nothing masked. + alignas(8) unsigned long long action[4] = {}; + call(nr_rt_sigaction, signal, reinterpret_cast(action), 0, 8); +} + +void raise_signal(int signal) { + unsigned long long set = 1ull << (signal - 1); + call(nr_rt_sigprocmask, sig_unblock, reinterpret_cast(&set), 0, 8); + call(nr_kill, call(nr_getpid), signal); +} + +} // namespace mcpplibs::os::kernel + +namespace mcpplibs::os::linux_kernel { + +long uname(utsname_t* out) { return call(nr_uname, reinterpret_cast(out)); } + +long sysinfo(sysinfo_t* out) { return call(nr_sysinfo, reinterpret_cast(out)); } + +long statfs_type(const char* path, unsigned long long* type) { + // `struct statfs' of a 64-bit kernel begins with `f_type'; 120 bytes, here 256. + alignas(8) unsigned long long st[32] = {}; + const long r = call(nr_statfs, reinterpret_cast(path), reinterpret_cast(st)); + if (r == 0) *type = st[0]; + return r; +} + +long renameat2(int from_dir, const char* from, int to_dir, const char* to, unsigned flags) { + return call(nr_renameat2, from_dir, reinterpret_cast(from), to_dir, reinterpret_cast(to), + static_cast(flags)); +} + +} // namespace mcpplibs::os::linux_kernel diff --git a/src/linux/arch/riscv64.cpp b/src/linux/arch/riscv64.cpp new file mode 100644 index 0000000..323a5c9 --- /dev/null +++ b/src/linux/arch/riscv64.cpp @@ -0,0 +1,85 @@ +// The kernel's calls on riscv64 Linux (src/posix/kernel.h, src/linux/kernel.h): +// the trap, and the numbers of the calls this package makes, from the kernel's +// include/uapi/asm-generic/unistd.h. +#include "../kernel.h" +#include "../../posix/kernel.h" + +namespace { + +long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { + register long a7 __asm__("a7") = n; + register long a0 __asm__("a0") = a; + register long a1 __asm__("a1") = b; + register long a2 __asm__("a2") = c; + register long a3 __asm__("a3") = d; + register long a4 __asm__("a4") = e; + __asm__ __volatile__("ecall" : "+r"(a0) : "r"(a7), "r"(a1), "r"(a2), "r"(a3), "r"(a4) : "memory"); + return a0; +} + +constexpr long nr_write = 64, + nr_close = 57, + nr_fcntl = 25, + nr_dup3 = 24, + nr_rt_sigaction = 134, + nr_rt_sigprocmask = 135, + nr_getpid = 172, + nr_kill = 129, + nr_uname = 160, + nr_sysinfo = 179, + nr_statfs = 43, + nr_renameat2 = 276; + +constexpr long f_dupfd_cloexec = 1030, sig_unblock = 1; + +} // namespace + +namespace mcpplibs::os::kernel { + +long dup_above(int fd, int lowest) { return call(nr_fcntl, fd, f_dupfd_cloexec, lowest); } + +long dup_onto(int from, int to) { + if (from == to) return call(nr_fcntl, from, 1 /* F_GETFD */) < 0 ? -9 /* EBADF */ : to; + return call(nr_dup3, from, to, 0); +} + +long write(int fd, const void* bytes, unsigned long count) { + return call(nr_write, fd, reinterpret_cast(bytes), static_cast(count)); +} + +long close(int fd) { return call(nr_close, fd); } + +void default_signal(int signal) { + // The kernel's `struct sigaction', zero: SIG_DFL, no flags, nothing masked. + alignas(8) unsigned long long action[4] = {}; + call(nr_rt_sigaction, signal, reinterpret_cast(action), 0, 8); +} + +void raise_signal(int signal) { + unsigned long long set = 1ull << (signal - 1); + call(nr_rt_sigprocmask, sig_unblock, reinterpret_cast(&set), 0, 8); + call(nr_kill, call(nr_getpid), signal); +} + +} // namespace mcpplibs::os::kernel + +namespace mcpplibs::os::linux_kernel { + +long uname(utsname_t* out) { return call(nr_uname, reinterpret_cast(out)); } + +long sysinfo(sysinfo_t* out) { return call(nr_sysinfo, reinterpret_cast(out)); } + +long statfs_type(const char* path, unsigned long long* type) { + // `struct statfs' of a 64-bit kernel begins with `f_type'; 120 bytes, here 256. + alignas(8) unsigned long long st[32] = {}; + const long r = call(nr_statfs, reinterpret_cast(path), reinterpret_cast(st)); + if (r == 0) *type = st[0]; + return r; +} + +long renameat2(int from_dir, const char* from, int to_dir, const char* to, unsigned flags) { + return call(nr_renameat2, from_dir, reinterpret_cast(from), to_dir, reinterpret_cast(to), + static_cast(flags)); +} + +} // namespace mcpplibs::os::linux_kernel diff --git a/src/linux/arch/x86_64.cpp b/src/linux/arch/x86_64.cpp new file mode 100644 index 0000000..0a03337 --- /dev/null +++ b/src/linux/arch/x86_64.cpp @@ -0,0 +1,85 @@ +// The kernel's calls on x86_64 Linux (src/posix/kernel.h, src/linux/kernel.h): +// the trap, and the numbers of the calls this package makes, from the kernel's +// arch/x86/entry/syscalls/syscall_64.tbl. +#include "../kernel.h" +#include "../../posix/kernel.h" + +namespace { + +long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { + long r; + register long r10 __asm__("r10") = d; + register long r8 __asm__("r8") = e; + __asm__ __volatile__("syscall" + : "=a"(r) + : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8) + : "rcx", "r11", "memory"); + return r; +} + +constexpr long nr_write = 1, + nr_close = 3, + nr_fcntl = 72, + nr_dup3 = 292, + nr_rt_sigaction = 13, + nr_rt_sigprocmask = 14, + nr_getpid = 39, + nr_kill = 62, + nr_uname = 63, + nr_sysinfo = 99, + nr_statfs = 137, + nr_renameat2 = 316; + +constexpr long f_dupfd_cloexec = 1030, sig_unblock = 1; + +} // namespace + +namespace mcpplibs::os::kernel { + +long dup_above(int fd, int lowest) { return call(nr_fcntl, fd, f_dupfd_cloexec, lowest); } + +long dup_onto(int from, int to) { + if (from == to) return call(nr_fcntl, from, 1 /* F_GETFD */) < 0 ? -9 /* EBADF */ : to; + return call(nr_dup3, from, to, 0); +} + +long write(int fd, const void* bytes, unsigned long count) { + return call(nr_write, fd, reinterpret_cast(bytes), static_cast(count)); +} + +long close(int fd) { return call(nr_close, fd); } + +void default_signal(int signal) { + // The kernel's `struct sigaction', zero: SIG_DFL, no flags, nothing masked. + alignas(8) unsigned long long action[4] = {}; + call(nr_rt_sigaction, signal, reinterpret_cast(action), 0, 8); +} + +void raise_signal(int signal) { + unsigned long long set = 1ull << (signal - 1); + call(nr_rt_sigprocmask, sig_unblock, reinterpret_cast(&set), 0, 8); + call(nr_kill, call(nr_getpid), signal); +} + +} // namespace mcpplibs::os::kernel + +namespace mcpplibs::os::linux_kernel { + +long uname(utsname_t* out) { return call(nr_uname, reinterpret_cast(out)); } + +long sysinfo(sysinfo_t* out) { return call(nr_sysinfo, reinterpret_cast(out)); } + +long statfs_type(const char* path, unsigned long long* type) { + // `struct statfs' of a 64-bit kernel begins with `f_type'; 120 bytes, here 256. + alignas(8) unsigned long long st[32] = {}; + const long r = call(nr_statfs, reinterpret_cast(path), reinterpret_cast(st)); + if (r == 0) *type = st[0]; + return r; +} + +long renameat2(int from_dir, const char* from, int to_dir, const char* to, unsigned flags) { + return call(nr_renameat2, from_dir, reinterpret_cast(from), to_dir, reinterpret_cast(to), + static_cast(flags)); +} + +} // namespace mcpplibs::os::linux_kernel diff --git a/src/linux/fs.cpp b/src/linux/fs.cpp index f7cc8cf..c053061 100644 --- a/src/linux/fs.cpp +++ b/src/linux/fs.cpp @@ -1,11 +1,7 @@ // Linux's own of mcpplibs.os.fs: an exchange (renameat2 RENAME_EXCHANGE) and -// the volume's kind (statfs). +// the volume's kind (statfs), by the kernel's calls (src/linux/kernel.h). module; -#include -#include -#include -#include -#include +#include "kernel.h" module mcpplibs.os.fs; @@ -13,16 +9,20 @@ import std; namespace mcpplibs::os::fs::sys { +namespace k = mcpplibs::os::linux_kernel; + int exchange(const std::filesystem::path& a, const std::filesystem::path& b) { - constexpr unsigned kExchange = 2; // RENAME_EXCHANGE - return ::syscall(SYS_renameat2, AT_FDCWD, a.c_str(), AT_FDCWD, b.c_str(), kExchange) == 0 ? 0 : errno; + return static_cast(-k::renameat2(k::at_cwd, a.c_str(), k::at_cwd, b.c_str(), k::rename_exchange)); +} +bool exchange_unsupported(int c) { + return c == static_cast(std::errc::invalid_argument) || c == static_cast(std::errc::function_not_supported) + || c == static_cast(std::errc::operation_not_supported); } -bool exchange_unsupported(int c) { return c == EINVAL || c == ENOSYS || c == EOPNOTSUPP; } std::string volume_type(const std::filesystem::path& directory) { - struct statfs st {}; - if (::statfs(directory.c_str(), &st) != 0) return {}; - switch (static_cast(st.f_type)) { + unsigned long long type = 0; + if (k::statfs_type(directory.c_str(), &type) != 0) return {}; + switch (type) { case 0xEF53: return "ext4"; case 0x58465342: return "xfs"; case 0x9123683E: return "btrfs"; @@ -39,7 +39,7 @@ std::string volume_type(const std::filesystem::path& directory) { case 0x01021997: return "9p"; case 0x2FC12FC1: return "zfs"; case 0x00C36400: return "ceph"; - default: return std::format("0x{:x}", static_cast(st.f_type)); + default: return std::format("0x{:x}", type); } } diff --git a/src/linux/kernel.h b/src/linux/kernel.h new file mode 100644 index 0000000..c98a82f --- /dev/null +++ b/src/linux/kernel.h @@ -0,0 +1,45 @@ +// The Linux kernel's own calls, for what this package's Linux units ask the +// system beside openkal: the machine's name, its memory, a volume's kind and +// an exchange of two entries. +// +// THE KERNEL AND NOT THE C LIBRARY. A program over this package chooses its C +// library, and that library may sit on openkal rather than on the kernel +// (openkal-musl, mcpp docs/24): its `uname' then names openkal, its `sysinfo' +// is not provided, and its `syscall' reaches openkal's port, not the kernel. +// The kernel's interface is the system's stable one and is the same under +// every C library, so the answers below are the system's in every +// arrangement. One file per architecture (src/linux/arch/) holds the trap and +// the numbers; the manifest selects it. +#pragma once + +namespace mcpplibs::os::linux_kernel { + +// `struct new_utsname'. +struct utsname_t { + char sysname[65], nodename[65], release[65], version[65], machine[65], domainname[65]; +}; + +// `struct sysinfo' of a 64-bit kernel: every field a 64-bit kernel `long' or +// as written, 112 bytes; the tail is room the kernel never fills. +struct sysinfo_t { + long long uptime; + unsigned long long loads[3]; + unsigned long long totalram, freeram, sharedram, bufferram, totalswap, freeswap; + unsigned short procs, pad; + unsigned long long totalhigh, freehigh; + unsigned int mem_unit; + char reserved[64]; +}; + +inline constexpr int at_cwd = -100; // AT_FDCWD +inline constexpr unsigned rename_exchange = 2; // RENAME_EXCHANGE + +// Each returns 0, or the negated error number, as the kernel does; the error +// numbers are the kernel's, which every C library on Linux uses. +long uname(utsname_t* out); +long sysinfo(sysinfo_t* out); +// The kind (`f_type') of the volume that holds `path'. +long statfs_type(const char* path, unsigned long long* type); +long renameat2(int from_dir, const char* from, int to_dir, const char* to, unsigned flags); + +} // namespace mcpplibs::os::linux_kernel diff --git a/src/linux/machine.cpp b/src/linux/machine.cpp index a702c4c..a4fa38f 100644 --- a/src/linux/machine.cpp +++ b/src/linux/machine.cpp @@ -1,10 +1,9 @@ +// mcpplibs.os.machine on Linux: the kernel's calls for the machine's name and +// memory (src/linux/kernel.h), and the kernel's files for the rest. A file is +// read through the program's C library, which reads it the same over every +// kernel layer. module; -#include -#include -#include -#include -#include -#include +#include "kernel.h" module mcpplibs.os.machine; @@ -12,82 +11,107 @@ import std; namespace mcpplibs::os::machine { -std::string arch() { - utsname u{}; - if (::uname(&u) != 0) return {}; - std::string m = u.machine; - if (m == "arm64") return "aarch64"; - return m; +namespace k = mcpplibs::os::linux_kernel; + +namespace { + +// The first line of a kernel file, or nothing. +std::string first_line(const char* path) { + std::ifstream in(path, std::ios::binary); + std::string line; + std::getline(in, line); + return line; } -std::optional memory() { - struct sysinfo s{}; - if (::sysinfo(&s) != 0) return std::nullopt; - return static_cast(s.totalram) * s.mem_unit; +// The value after `key' in /proc/meminfo, in bytes. +std::optional meminfo(std::string_view key) { + std::ifstream in("/proc/meminfo", std::ios::binary); + for (std::string line; std::getline(in, line);) { + if (!line.starts_with(key)) continue; + std::size_t i = key.size(); + while (i < line.size() && line[i] == ' ') ++i; + std::uint64_t kb = 0; + const auto [end, ec] = std::from_chars(line.data() + i, line.data() + line.size(), kb); + if (ec != std::errc{}) return std::nullopt; + return kb * 1024u; + } + return std::nullopt; } -namespace { -bool meminfo(const char* key, std::uint64_t& out) { - FILE* f = ::fopen("/proc/meminfo", "r"); - if (!f) return false; - char line[256]; - bool found = false; - const std::size_t klen = ::strlen(key); - while (::fgets(line, sizeof line, f)) { - if (::strncmp(line, key, klen) != 0) continue; - unsigned long long kb = 0; - if (::sscanf(line + klen, " %llu", &kb) == 1) { out = kb * 1024ull; found = true; } - break; +// The processors online: a list of ranges, `0-3,8-11'. +std::uint32_t online(std::string_view list) { + std::uint32_t n = 0; + while (!list.empty()) { + const auto comma = list.find(','); + const auto part = list.substr(0, comma); + unsigned lo = 0, hi = 0; + const auto dash = part.find('-'); + const auto [e1, c1] = std::from_chars(part.data(), part.data() + std::min(dash, part.size()), lo); + hi = lo; + if (dash != std::string_view::npos) std::from_chars(part.data() + dash + 1, part.data() + part.size(), hi); + if (c1 == std::errc{} && hi >= lo) n += hi - lo + 1; + if (comma == std::string_view::npos) break; + list.remove_prefix(comma + 1); } - ::fclose(f); - return found; + return n; } + int cores_per_package() { - FILE* f = ::fopen("/proc/cpuinfo", "r"); - if (!f) return 0; - char line[256]; - int cores = 0; - while (::fgets(line, sizeof line, f)) { - if (::strncmp(line, "cpu cores", 9) != 0) continue; - if (const char* colon = ::strchr(line, ':')) cores = ::atoi(colon + 1); - break; + std::ifstream in("/proc/cpuinfo", std::ios::binary); + for (std::string line; std::getline(in, line);) { + if (!line.starts_with("cpu cores")) continue; + const auto colon = line.find(':'); + if (colon == std::string::npos) return 0; + std::size_t i = colon + 1; + while (i < line.size() && line[i] == ' ') ++i; + int cores = 0; + std::from_chars(line.data() + i, line.data() + line.size(), cores); + return cores; } - ::fclose(f); - return cores; + return 0; } + // Cores whose top frequencies differ: performance and efficiency cores. -bool hybrid(long logical) { +bool hybrid(std::uint32_t logical) { if (logical <= 1) return false; - long first = -1; - for (long i = 0; i < logical; ++i) { - char path[128]; - ::snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%ld/cpufreq/cpuinfo_max_freq", i); - FILE* f = ::fopen(path, "r"); - if (!f) return false; - long v = 0; - const int got = ::fscanf(f, "%ld", &v); - ::fclose(f); - if (got != 1) return false; - if (first < 0) first = v; + std::string first; + for (std::uint32_t i = 0; i < logical; ++i) { + const auto v = first_line(std::format("/sys/devices/system/cpu/cpu{}/cpufreq/cpuinfo_max_freq", i).c_str()); + if (v.empty()) return false; + if (first.empty()) first = v; else if (v != first) return true; } return false; } + } // namespace +std::string arch() { + k::utsname_t u{}; + if (k::uname(&u) != 0) return {}; + std::string m = u.machine; + if (m == "arm64") return "aarch64"; + return m; +} + +std::optional memory() { + k::sysinfo_t s{}; + if (k::sysinfo(&s) != 0) return std::nullopt; + return s.totalram * (s.mem_unit ? s.mem_unit : 1u); +} + std::optional memory_available() { - std::uint64_t v = 0; - if (meminfo("MemAvailable:", v)) return v; + if (const auto v = meminfo("MemAvailable:")) return v; return memory(); } processor_count processors() { processor_count c; - const long n = ::sysconf(_SC_NPROCESSORS_ONLN); - if (n > 0) c.logical = static_cast(n); + c.logical = online(first_line("/sys/devices/system/cpu/online")); + if (c.logical == 0) c.logical = std::max(1u, std::thread::hardware_concurrency()); const int cores = cores_per_package(); - c.physical = cores > 0 ? static_cast(cores) : c.logical; - c.heterogeneous = hybrid(static_cast(c.logical)); + c.physical = cores > 0 ? std::min(static_cast(cores), c.logical) : c.logical; + c.heterogeneous = hybrid(c.logical); return c; } diff --git a/src/macos/arch/aarch64.cpp b/src/macos/arch/aarch64.cpp new file mode 100644 index 0000000..22c9d27 --- /dev/null +++ b/src/macos/arch/aarch64.cpp @@ -0,0 +1,72 @@ +// The kernel's calls on aarch64 macOS (src/posix/kernel.h, src/macos/kernel.h): the trap, and the +// numbers of the calls this package makes, from the kernel's +// bsd/kern/syscalls.master --- the same on both architectures, and the ones +// openkal-macos issues. +#include "../kernel.h" +#include "../../posix/kernel.h" + +namespace { + +long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { + register long x16 __asm__("x16") = n; + register long x0 __asm__("x0") = a; + register long x1 __asm__("x1") = b; + register long x2 __asm__("x2") = c; + register long x3 __asm__("x3") = d; + register long x4 __asm__("x4") = e; + long failed; + // The carry flag reports a failure and x0 is then the error number; x1 + // comes back too, so it is an output as well. + __asm__ __volatile__("svc #0x80\n\tcset %2, cs" + : "+r"(x0), "+r"(x1), "=r"(failed) + : "r"(x16), "r"(x2), "r"(x3), "r"(x4) + : "memory", "cc"); + return failed ? -x0 : x0; +} + +constexpr long nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, + nr_sigprocmask = 48, nr_dup2 = 90, nr_fcntl = 92, nr_statfs64 = 345, nr_renameatx_np = 488; + +constexpr long f_dupfd_cloexec = 67, sig_unblock = 2; + +} // namespace + +namespace mcpplibs::os::kernel { + +long dup_above(int fd, int lowest) { return call(nr_fcntl, fd, f_dupfd_cloexec, lowest); } + +long dup_onto(int from, int to) { return call(nr_dup2, from, to); } + +long write(int fd, const void* bytes, unsigned long count) { + return call(nr_write, fd, reinterpret_cast(bytes), static_cast(count)); +} + +long close(int fd) { return call(nr_close, fd); } + +void default_signal(int signal) { + // The kernel's `struct __sigaction' (handler, trampoline, mask, flags), + // zero: SIG_DFL, which the kernel enters no trampoline for. + struct { void* handler; void* trampoline; unsigned int mask; int flags; } action{}; + call(nr_sigaction, signal, reinterpret_cast(&action), 0); +} + +void raise_signal(int signal) { + unsigned int set = 1u << (signal - 1); + call(nr_sigprocmask, sig_unblock, reinterpret_cast(&set), 0); + call(nr_kill, call(nr_getpid), signal, 1 /* POSIX */); +} + +} // namespace mcpplibs::os::kernel + +namespace mcpplibs::os::macos_kernel { + +long statfs(const char* path, statfs_t* out) { + return call(nr_statfs64, reinterpret_cast(path), reinterpret_cast(out)); +} + +long renameatx_np(int from_dir, const char* from, int to_dir, const char* to, unsigned flags) { + return call(nr_renameatx_np, from_dir, reinterpret_cast(from), to_dir, reinterpret_cast(to), + static_cast(flags)); +} + +} // namespace mcpplibs::os::macos_kernel diff --git a/src/macos/arch/x86_64.cpp b/src/macos/arch/x86_64.cpp new file mode 100644 index 0000000..eac1c95 --- /dev/null +++ b/src/macos/arch/x86_64.cpp @@ -0,0 +1,69 @@ +// The kernel's calls on x86_64 macOS (src/posix/kernel.h, src/macos/kernel.h): the trap, and the +// numbers of the calls this package makes, from the kernel's +// bsd/kern/syscalls.master --- the same on both architectures, and the ones +// openkal-macos issues. +#include "../kernel.h" +#include "../../posix/kernel.h" + +namespace { + +long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { + long r; + unsigned char failed; + register long r10 __asm__("r10") = d; + register long r8 __asm__("r8") = e; + // The class of the ordinary calls (2) in the number's high bits; the + // carry flag reports a failure and the result is then the error number. + __asm__ __volatile__("syscall" + : "=a"(r), "+d"(c), "=@ccc"(failed) + : "a"(n | 0x2000000L), "D"(a), "S"(b), "r"(r10), "r"(r8) + : "rcx", "r11", "memory", "cc"); + return failed ? -r : r; +} + +constexpr long nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, + nr_sigprocmask = 48, nr_dup2 = 90, nr_fcntl = 92, nr_statfs64 = 345, nr_renameatx_np = 488; + +constexpr long f_dupfd_cloexec = 67, sig_unblock = 2; + +} // namespace + +namespace mcpplibs::os::kernel { + +long dup_above(int fd, int lowest) { return call(nr_fcntl, fd, f_dupfd_cloexec, lowest); } + +long dup_onto(int from, int to) { return call(nr_dup2, from, to); } + +long write(int fd, const void* bytes, unsigned long count) { + return call(nr_write, fd, reinterpret_cast(bytes), static_cast(count)); +} + +long close(int fd) { return call(nr_close, fd); } + +void default_signal(int signal) { + // The kernel's `struct __sigaction' (handler, trampoline, mask, flags), + // zero: SIG_DFL, which the kernel enters no trampoline for. + struct { void* handler; void* trampoline; unsigned int mask; int flags; } action{}; + call(nr_sigaction, signal, reinterpret_cast(&action), 0); +} + +void raise_signal(int signal) { + unsigned int set = 1u << (signal - 1); + call(nr_sigprocmask, sig_unblock, reinterpret_cast(&set), 0); + call(nr_kill, call(nr_getpid), signal, 1 /* POSIX */); +} + +} // namespace mcpplibs::os::kernel + +namespace mcpplibs::os::macos_kernel { + +long statfs(const char* path, statfs_t* out) { + return call(nr_statfs64, reinterpret_cast(path), reinterpret_cast(out)); +} + +long renameatx_np(int from_dir, const char* from, int to_dir, const char* to, unsigned flags) { + return call(nr_renameatx_np, from_dir, reinterpret_cast(from), to_dir, reinterpret_cast(to), + static_cast(flags)); +} + +} // namespace mcpplibs::os::macos_kernel diff --git a/src/macos/darwin.h b/src/macos/darwin.h new file mode 100644 index 0000000..02d88d9 --- /dev/null +++ b/src/macos/darwin.h @@ -0,0 +1,37 @@ +// The functions of libSystem this package's macOS units call, declared here +// rather than taken from the SDK's headers (as src/windows/win32.h does for +// Windows): the units then compile the same under the system's C library and +// under one that sits on openkal (openkal-musl, mcpp docs/24), whose headers +// are not the SDK's. +// +// EVERY NAME IS ONE NO C LIBRARY DEFINES, so it reaches libSystem whichever C +// library the program links; a name both define resolves to the program's own. +// What is a kernel call is made as one instead (src/macos/kernel.h). +// +// AND EACH MAY BE ABSENT FROM THE LINK. A build pointed at a target links +// against openkal-macos's stub of libSystem, which lists only the names +// openkal-macos calls; the manifest lets these be undefined there (`-U'), and +// the loader finds them in libSystem, which every program on this system loads. +// A link against the SDK binds them as usual. +#pragma once + +extern "C" { + +int sysctlbyname(const char* name, void* value, unsigned long* length, void* replacement, unsigned long size); + +// Mach: the host's port, its page size and its memory statistics (HOST_VM_INFO64 = 4). +unsigned int mach_host_self(void); +int host_page_size(unsigned int host, unsigned long* size); +int host_statistics64(unsigned int host, int flavor, int* info, unsigned int* count); + +// `vm_statistics64_data_t' (mach/vm_statistics.h): 38 words. +struct okos_vm_statistics64 { + unsigned int free_count, active_count, inactive_count, wire_count; + unsigned long long zero_fill_count, reactivations, pageins, pageouts, faults, cow_faults, lookups, hits, purges; + unsigned int purgeable_count, speculative_count; + unsigned long long decompressions, compressions, swapins, swapouts; + unsigned int compressor_page_count, throttled_count, external_page_count, internal_page_count; + unsigned long long total_uncompressed_pages_in_compressor; +}; + +} diff --git a/src/macos/fs.cpp b/src/macos/fs.cpp index bfbb4ad..ae05899 100644 --- a/src/macos/fs.cpp +++ b/src/macos/fs.cpp @@ -1,10 +1,8 @@ -// macOS's own of mcpplibs.os.fs: an exchange (renamex_np RENAME_SWAP) and the -// volume's kind (statfs f_fstypename). +// macOS's own of mcpplibs.os.fs: an exchange (renameatx_np RENAME_SWAP) and the +// volume's kind (statfs64 f_fstypename), by the kernel's calls +// (src/macos/kernel.h). module; -#include -#include -#include -#include +#include "kernel.h" module mcpplibs.os.fs; @@ -12,14 +10,41 @@ import std; namespace mcpplibs::os::fs::sys { +namespace k = mcpplibs::os::macos_kernel; + +namespace { + +// This system's error number as the program's C library numbers it. The first +// thirty-four agree everywhere; the others this unit can meet are named. +int condition_of(long darwin) { + if (darwin <= 34) return static_cast(darwin); + switch (darwin) { + case 45: return static_cast(std::errc::not_supported); // ENOTSUP + case 63: return static_cast(std::errc::filename_too_long); // ENAMETOOLONG + case 66: return static_cast(std::errc::directory_not_empty); // ENOTEMPTY + case 78: return static_cast(std::errc::function_not_supported); // ENOSYS + case 102: return static_cast(std::errc::operation_not_supported); // EOPNOTSUPP + default: return static_cast(std::errc::io_error); + } +} + +} // namespace + int exchange(const std::filesystem::path& a, const std::filesystem::path& b) { - return ::renamex_np(a.c_str(), b.c_str(), RENAME_SWAP) == 0 ? 0 : errno; + const long r = k::renameatx_np(k::at_cwd, a.c_str(), k::at_cwd, b.c_str(), k::rename_swap); + return r == 0 ? 0 : condition_of(-r); +} +bool exchange_unsupported(int c) { + return c == static_cast(std::errc::invalid_argument) || c == static_cast(std::errc::not_supported) + || c == static_cast(std::errc::operation_not_supported) + || c == static_cast(std::errc::function_not_supported); } -bool exchange_unsupported(int c) { return c == EINVAL || c == ENOTSUP || c == EOPNOTSUPP || c == ENOSYS; } std::string volume_type(const std::filesystem::path& directory) { - struct statfs st {}; - return ::statfs(directory.c_str(), &st) == 0 ? std::string(st.f_fstypename) : std::string{}; + k::statfs_t st{}; + if (k::statfs(directory.c_str(), &st) != 0) return {}; + const std::string_view name(st.fstypename, sizeof st.fstypename); + return std::string(name.substr(0, name.find(char{}))); } } // namespace mcpplibs::os::fs::sys diff --git a/src/macos/kernel.h b/src/macos/kernel.h new file mode 100644 index 0000000..e23cca4 --- /dev/null +++ b/src/macos/kernel.h @@ -0,0 +1,30 @@ +// macOS's kernel calls for what this package's macOS units ask beside openkal: +// a volume's kind and an exchange of two entries, the calls openkal-macos +// itself issues for its own questions about volumes and renames. Defined in +// src/macos/arch/ with the calls of src/posix/kernel.h. +#pragma once + +namespace mcpplibs::os::macos_kernel { + +// `struct statfs64' (sys/mount.h, 64-bit inodes). +struct statfs_t { + unsigned int bsize; + int iosize; + unsigned long long blocks, bfree, bavail, files, ffree; + int fsid[2]; + unsigned int owner, type, flags, fssubtype; + char fstypename[16]; + char mntonname[1024]; + char mntfromname[1024]; + unsigned int flags_ext; + unsigned int reserved[7]; +}; + +inline constexpr int at_cwd = -2; // AT_FDCWD +inline constexpr unsigned rename_swap = 0x2; // RENAME_SWAP + +// Each returns 0, or the negated error number in this system's numbering. +long statfs(const char* path, statfs_t* out); +long renameatx_np(int from_dir, const char* from, int to_dir, const char* to, unsigned flags); + +} // namespace mcpplibs::os::macos_kernel diff --git a/src/macos/machine.cpp b/src/macos/machine.cpp index 4024288..a238ae5 100644 --- a/src/macos/machine.cpp +++ b/src/macos/machine.cpp @@ -1,8 +1,7 @@ +// mcpplibs.os.machine on macOS: sysctl and the host's statistics, through +// libSystem (src/macos/darwin.h). module; -#include -#include -#include -#include +#include "darwin.h" module mcpplibs.os.machine; @@ -14,7 +13,7 @@ namespace { template std::optional sysctl_value(const char* name) { T v{}; - std::size_t len = sizeof v; + unsigned long len = sizeof v; if (::sysctlbyname(name, &v, &len, nullptr, 0) != 0) return std::nullopt; return v; } @@ -25,7 +24,7 @@ std::string arch() { // whether it is translated: the translation is to x86_64 from arm64. if (sysctl_value("sysctl.proc_translated").value_or(0) == 1) return "aarch64"; char m[64] = {}; - std::size_t len = sizeof m; + unsigned long len = sizeof m; if (::sysctlbyname("hw.machine", m, &len, nullptr, 0) != 0) return {}; std::string s(m); return s == "arm64" ? "aarch64" : s; @@ -34,21 +33,21 @@ std::string arch() { std::optional memory() { return sysctl_value("hw.memsize"); } std::optional memory_available() { - vm_statistics64_data_t vm{}; - mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; - if (::host_statistics64(::mach_host_self(), HOST_VM_INFO64, reinterpret_cast(&vm), &count) - != KERN_SUCCESS) + const unsigned int host = ::mach_host_self(); + okos_vm_statistics64 vm{}; + unsigned int count = sizeof vm / sizeof(int); + unsigned long page = 0; + if (::host_statistics64(host, 4 /* HOST_VM_INFO64 */, reinterpret_cast(&vm), &count) != 0 + || ::host_page_size(host, &page) != 0 || page == 0) return memory(); - const auto page = static_cast(::getpagesize()); return (static_cast(vm.free_count) + static_cast(vm.inactive_count)) * page; } processor_count processors() { processor_count c; - const long n = ::sysconf(_SC_NPROCESSORS_ONLN); - if (n > 0) c.logical = static_cast(n); + c.logical = static_cast(std::max(1, sysctl_value("hw.activecpu").value_or(1))); const int physical = sysctl_value("hw.physicalcpu").value_or(0); - c.physical = physical > 0 ? static_cast(physical) : c.logical; + c.physical = physical > 0 ? std::min(static_cast(physical), c.logical) : c.logical; c.heterogeneous = sysctl_value("hw.nperflevels").value_or(1) > 1; return c; } diff --git a/src/macos/macos.cpp b/src/macos/macos.cpp index e42673e..ae39ec6 100644 --- a/src/macos/macos.cpp +++ b/src/macos/macos.cpp @@ -1,5 +1,5 @@ module; -#include +#include "darwin.h" module mcpplibs.os.macos; @@ -9,7 +9,7 @@ namespace mcpplibs::os::macos { bool translated() { int v = 0; - std::size_t len = sizeof v; + unsigned long len = sizeof v; return ::sysctlbyname("sysctl.proc_translated", &v, &len, nullptr, 0) == 0 && v == 1; } diff --git a/src/posix/exec.cpp b/src/posix/exec.cpp index 3454239..675f515 100644 --- a/src/posix/exec.cpp +++ b/src/posix/exec.cpp @@ -3,8 +3,8 @@ // waiter sees is the program's. module; #include -#include #include +#include "kernel.h" module mcpplibs.os.process; @@ -31,10 +31,9 @@ result replace_image(const std::filesystem::path& program, // fatal, and an ignored disposition survives execve: the program this one // becomes would begin with it ignored, and `yes | head' inside it would // report a write error instead of ending. A handler is reset by execve, an - // ignore is not, so the ignore is undone here. - struct sigaction pipe_default {}; - pipe_default.sa_handler = SIG_DFL; - ::sigaction(SIGPIPE, &pipe_default, nullptr); + // ignore is not, so the ignore is undone here, in the kernel, where the + // process keeps it (src/posix/kernel.h). + mcpplibs::os::kernel::default_signal(mcpplibs::os::kernel::sigpipe); ::execve(program.c_str(), args.data(), envp.data()); const int e = errno; // A file this machine cannot start: no image at all (ENOEXEC), or on diff --git a/src/posix/interrupt.cpp b/src/posix/interrupt.cpp index d3922db..5bb6e37 100644 --- a/src/posix/interrupt.cpp +++ b/src/posix/interrupt.cpp @@ -1,16 +1,20 @@ -// The system's ending for a closed reader: SIGPIPE at its default, raised. +// The system's ending for a closed reader: SIGPIPE at its default, raised, by +// the kernel's calls (src/posix/kernel.h). openkal ignores the signal in this +// program so that a write to a closed reader is reported; the disposition is +// the process's, so it is restored where the process keeps it. module; -#include -#include +#include "kernel.h" module mcpplibs.os.interrupt; +import std; + namespace mcpplibs::os::interrupt::sys { void end_as_output_closed() { - std::signal(SIGPIPE, SIG_DFL); - std::raise(SIGPIPE); - ::_exit(128 + SIGPIPE); + mcpplibs::os::kernel::default_signal(mcpplibs::os::kernel::sigpipe); + mcpplibs::os::kernel::raise_signal(mcpplibs::os::kernel::sigpipe); + std::_Exit(128 + mcpplibs::os::kernel::sigpipe); } } // namespace mcpplibs::os::interrupt::sys diff --git a/src/posix/kernel.h b/src/posix/kernel.h new file mode 100644 index 0000000..2bb0859 --- /dev/null +++ b/src/posix/kernel.h @@ -0,0 +1,33 @@ +// The descriptors and the signals of the process, by the kernel's own calls: +// what the units in src/posix/ do to the process rather than to the program's +// C library. +// +// A C library keeps descriptors and dispositions of its own when it sits on +// openkal rather than on the kernel (openkal-musl, mcpp docs/24): `dup2' then +// moves the library's descriptor and not the one openkal's standard streams +// and a started program use, and `signal' changes nothing the kernel acts on. +// Diverting the output and ending as a closed reader ends are acts upon the +// process, so they are made where the process is: in the kernel. Under the +// system's own C library the two coincide, and the result is the same. +// +// Each returns what the call returned, or the negated error number; the +// numbers are the system's (EINTR is 4 on both). Defined per system and +// architecture in src/linux/arch/ and src/macos/arch/. +#pragma once + +namespace mcpplibs::os::kernel { + +// A new descriptor for `fd', the lowest not below `lowest', closed on exec. +long dup_above(int fd, int lowest); +// `to' made a descriptor for what `from' is, closed first if open. +long dup_onto(int from, int to); +long write(int fd, const void* bytes, unsigned long count); +long close(int fd); +// The signal at its default disposition. +void default_signal(int signal); +// The signal unblocked in this thread and sent to this process. +void raise_signal(int signal); + +inline constexpr int sigpipe = 13; // the same number on Linux and macOS + +} // namespace mcpplibs::os::kernel diff --git a/src/posix/stdio.cpp b/src/posix/stdio.cpp index c2fcd4b..0455128 100644 --- a/src/posix/stdio.cpp +++ b/src/posix/stdio.cpp @@ -1,8 +1,8 @@ -// The output descriptor saved (not inherited) and replaced by the error one. +// The process's output descriptor saved (closed on exec, so not inherited) and +// replaced by the error one, by the kernel's calls (src/posix/kernel.h): what +// openkal's standard output, the C library's and a started program's all are. module; -#include -#include -#include +#include "kernel.h" module mcpplibs.os.stdio; @@ -10,22 +10,25 @@ import std; namespace mcpplibs::os::stdio::sys { +namespace k = mcpplibs::os::kernel; + int divert_output() { - const int saved = ::fcntl(1, F_DUPFD_CLOEXEC, 3); - if (saved >= 0) ::dup2(2, 1); - return saved; + const long saved = k::dup_above(1, 3); + if (saved < 0) return -1; + k::dup_onto(2, 1); + return static_cast(saved); } void restore_output(int saved) { - ::dup2(saved, 1); - ::close(saved); + k::dup_onto(saved, 1); + k::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; + const long n = k::write(descriptor, bytes.data() + done, bytes.size() - done); + if (n == -4 /* EINTR */) continue; if (n <= 0) return false; done += static_cast(n); } diff --git a/src/windows/stdio.cpp b/src/windows/crt/stdio.cpp similarity index 57% rename from src/windows/stdio.cpp rename to src/windows/crt/stdio.cpp index 295270a..27ff7d9 100644 --- a/src/windows/stdio.cpp +++ b/src/windows/crt/stdio.cpp @@ -1,8 +1,19 @@ -// The C runtime's output descriptor saved (its handle not inherited) and -// replaced by the error one; `_dup2' to a standard descriptor also sets the -// process's standard handle, which a started program receives. +// The output descriptor saved (its handle not inherited) and replaced by the +// error one, under the Microsoft C runtime: `_dup2' to a standard descriptor +// also sets the process's standard handle, which openkal's standard output and +// a started program use. Selected where the C library is the system's (the +// manifest); src/windows/stdio.cpp serves a C library on openkal. module; -#include "win32.h" +#include "../win32.h" + +extern "C" { +// The C runtime's descriptors. +int __cdecl _dup(int); +int __cdecl _dup2(int, int); +int __cdecl _close(int); +long long __cdecl _get_osfhandle(int); +int __cdecl _write(int, const void*, unsigned); +} module mcpplibs.os.stdio; diff --git a/src/windows/fs.cpp b/src/windows/fs.cpp index 3cadffc..1c0b04f 100644 --- a/src/windows/fs.cpp +++ b/src/windows/fs.cpp @@ -8,6 +8,8 @@ module mcpplibs.os.fs; import std; import mcpplibs.os.error; +#include "text.h" + namespace mcpplibs::os::fs::sys { namespace { @@ -18,7 +20,7 @@ okos_handle invalid() { return reinterpret_cast(static_cast sync(const std::filesystem::path& node) { std::error_code ec; if (std::filesystem::is_directory(node, ec)) return {}; // nothing of a directory is flushed here - const okos_handle h = CreateFileW(node.c_str(), 0x40000000 /* GENERIC_WRITE */, 0x7 /* share all */, + const okos_handle h = CreateFileW(utf16_from_path(node).c_str(), 0x40000000 /* GENERIC_WRITE */, 0x7 /* share all */, nullptr, 3 /* OPEN_EXISTING */, 0x80 /* NORMAL */, nullptr); if (h == invalid() || h == nullptr) { const auto e = GetLastError(); @@ -41,19 +43,19 @@ bool exchange_unsupported(int c) { } std::string volume_type(const std::filesystem::path& directory) { - wchar_t root[260] {}, name[260] {}; - if (!GetVolumePathNameW(directory.c_str(), root, 260)) return {}; - if (!GetVolumeInformationW(root, nullptr, 0, nullptr, nullptr, nullptr, name, 260)) return {}; - return mcpplibs::os::fs::utf8(std::filesystem::path(name)); + okos_wchar root[260] {}, name[260] {}; + if (!late::GetVolumePathNameW(utf16_from_path(directory).c_str(), root, 260)) return {}; + if (!late::GetVolumeInformationW(root, nullptr, 0, nullptr, nullptr, nullptr, name, 260)) return {}; + return mcpplibs::os::fs::utf8(path_from_utf16(name)); } owner owner_of(const std::filesystem::path& node) { - using named_security = okos_dword (__stdcall*)(const wchar_t*, int, okos_dword, void**, void**, void**, void**, void**); + using named_security = okos_dword (__stdcall*)(const okos_wchar*, int, okos_dword, void**, void**, void**, void**, void**); using well_known = okos_bool (__stdcall*)(void*, int); using open_token = okos_bool (__stdcall*)(okos_handle, okos_dword, okos_handle*); using token_information = okos_bool (__stdcall*)(okos_handle, int, void*, okos_dword, okos_dword*); using equal_sid = okos_bool (__stdcall*)(void*, void*); - const okos_handle advapi = LoadLibraryW(L"advapi32.dll"); + const okos_handle advapi = LoadLibraryW(u"advapi32.dll"); if (!advapi) return owner::unknown; const auto security = reinterpret_cast(GetProcAddress(advapi, "GetNamedSecurityInfoW")); const auto known = reinterpret_cast(GetProcAddress(advapi, "IsWellKnownSid")); @@ -63,7 +65,7 @@ owner owner_of(const std::filesystem::path& node) { if (!security || !known || !open || !query || !equal) return owner::unknown; void* who = nullptr; void* descriptor = nullptr; - if (security(node.c_str(), 1 /* SE_FILE_OBJECT */, 0x1 /* OWNER_SECURITY_INFORMATION */, + if (security(utf16_from_path(node).c_str(), 1 /* SE_FILE_OBJECT */, 0x1 /* OWNER_SECURITY_INFORMATION */, &who, nullptr, nullptr, nullptr, &descriptor) != 0) return owner::unknown; owner found = owner::another_user; diff --git a/src/windows/library.cpp b/src/windows/library.cpp index b34bfad..073a6c7 100644 --- a/src/windows/library.cpp +++ b/src/windows/library.cpp @@ -8,22 +8,13 @@ import std; import mcpplibs.os.error; import mcpplibs.os.fs; -namespace mcpplibs::os::library { - -namespace { +#include "text.h" -std::wstring wide(std::string_view utf8) { - const int n = MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); - std::wstring out(static_cast(n > 0 ? n : 0), L'\0'); - if (n > 0) MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), out.data(), n); - return out; -} - -} // namespace +namespace mcpplibs::os::library { handle& handle::operator=(handle&& other) noexcept { if (this != &other) { - if (owned_ && native_) FreeLibrary(native_); + if (owned_ && native_) late::FreeLibrary(native_); native_ = std::exchange(other.native_, nullptr); owned_ = std::exchange(other.owned_, false); } @@ -31,7 +22,7 @@ handle& handle::operator=(handle&& other) noexcept { } handle::~handle() { - if (owned_ && native_) FreeLibrary(native_); + if (owned_ && native_) late::FreeLibrary(native_); } void* handle::symbol(std::string_view name) const { @@ -42,14 +33,14 @@ void* handle::symbol(std::string_view name) const { result open(const std::filesystem::path& file) { const std::string name = fs::utf8(file); - okos_handle h = LoadLibraryW(wide(name).c_str()); + okos_handle h = LoadLibraryW(utf16_from_utf8(name).c_str()); if (!h) return std::unexpected(error{errc::not_found, std::format("loading {}", name), static_cast(GetLastError())}); return handle(h, true); } std::optional loaded(std::string_view name) { // GetModuleHandleW counts no reference: the handle closes nothing. - okos_handle h = GetModuleHandleW(wide(name).c_str()); + okos_handle h = late::GetModuleHandleW(utf16_from_utf8(name).c_str()); if (!h) return std::nullopt; return handle(h, false); } @@ -58,7 +49,7 @@ void* process_symbol(std::string_view name) { const std::string n(name); okos_handle modules[1024]; okos_dword bytes = 0; - if (!K32EnumProcessModules(GetCurrentProcess(), modules, sizeof modules, &bytes)) return nullptr; + if (!late::K32EnumProcessModules(GetCurrentProcess(), modules, sizeof modules, &bytes)) return nullptr; const std::size_t count = std::min(bytes / sizeof(okos_handle), std::size(modules)); for (std::size_t i = 0; i < count; ++i) if (void* p = GetProcAddress(modules[i], n.c_str())) return p; diff --git a/src/windows/machine.cpp b/src/windows/machine.cpp index ad308f7..11c8790 100644 --- a/src/windows/machine.cpp +++ b/src/windows/machine.cpp @@ -9,7 +9,7 @@ namespace mcpplibs::os::machine { std::string arch() { unsigned short process = 0, native = 0; - if (!IsWow64Process2(GetCurrentProcess(), &process, &native)) return {}; + if (!late::IsWow64Process2(GetCurrentProcess(), &process, &native)) return {}; switch (native) { case 0x8664: return "x86_64"; // IMAGE_FILE_MACHINE_AMD64 case 0xAA64: return "aarch64"; // IMAGE_FILE_MACHINE_ARM64 @@ -21,29 +21,29 @@ std::string arch() { std::optional memory() { okos_memory_status s{}; s.length = sizeof s; - if (!GlobalMemoryStatusEx(&s)) return std::nullopt; + if (!late::GlobalMemoryStatusEx(&s)) return std::nullopt; return s.total_physical; } std::optional memory_available() { okos_memory_status s{}; s.length = sizeof s; - if (!GlobalMemoryStatusEx(&s)) return std::nullopt; + if (!late::GlobalMemoryStatusEx(&s)) return std::nullopt; return s.available_physical; } processor_count processors() { processor_count c; - const auto n = GetActiveProcessorCount(0xffff /* ALL_PROCESSOR_GROUPS */); + const auto n = late::GetActiveProcessorCount(0xffff /* ALL_PROCESSOR_GROUPS */); if (n > 0) c.logical = static_cast(n); c.physical = c.logical; // One record per core (RelationProcessorCore): the count, and an // efficiency class that differs between cores of differing speed. okos_dword bytes = 0; - GetLogicalProcessorInformationEx(0 /* RelationProcessorCore */, nullptr, &bytes); + late::GetLogicalProcessorInformationEx(0 /* RelationProcessorCore */, nullptr, &bytes); if (bytes == 0) return c; std::vector buf(bytes); - if (!GetLogicalProcessorInformationEx(0, buf.data(), &bytes)) return c; + if (!late::GetLogicalProcessorInformationEx(0, buf.data(), &bytes)) return c; std::uint32_t count = 0; int first = -1; for (okos_dword off = 0; off + 8 < bytes;) { diff --git a/src/windows/posix/stdio.cpp b/src/windows/posix/stdio.cpp new file mode 100644 index 0000000..ccf5e31 --- /dev/null +++ b/src/windows/posix/stdio.cpp @@ -0,0 +1,65 @@ +// The output diverted for a C library that presents POSIX on Windows over +// openkal (openkal-musl): its descriptor 1 is saved (closed on exec) and made +// one for the error output, and the process's standard output handle, which +// openkal's standard output and a started program use, is given the standard +// error's for as long, the handle it had kept not inherited meanwhile. The C +// library holds the handle its descriptor 1 began with, so neither change +// reaches what the other does. Selected by the manifest for such a library; +// src/windows/crt/stdio.cpp serves the Microsoft C runtime. +module; +#include +#include +#include +#include "../win32.h" + +module mcpplibs.os.stdio; + +import std; + +namespace mcpplibs::os::stdio::sys { + +namespace { +constexpr okos_dword kOutput = static_cast(-11), kError = static_cast(-12); +constexpr okos_dword kInherit = 0x1; // HANDLE_FLAG_INHERIT +struct kept { okos_handle handle; okos_dword flags; }; +std::mutex guard; +std::vector given; // the standard output handles to give back, innermost last +} // namespace + +int divert_output() { + const int saved = ::fcntl(1, F_DUPFD_CLOEXEC, 3); + if (saved < 0) return -1; + ::dup2(2, 1); + const std::lock_guard lock(guard); + const okos_handle output = GetStdHandle(kOutput); + okos_dword flags = 0; + if (!GetHandleInformation(output, &flags)) flags = 0; + given.push_back({ output, flags }); + SetHandleInformation(output, kInherit, 0); + late::SetStdHandle(kOutput, GetStdHandle(kError)); + return saved; +} + +void restore_output(int saved) { + ::dup2(saved, 1); + ::close(saved); + const std::lock_guard lock(guard); + if (given.empty()) return; + const kept k = given.back(); + given.pop_back(); + late::SetStdHandle(kOutput, k.handle); + SetHandleInformation(k.handle, kInherit, k.flags & kInherit); +} + +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/windows/text.h b/src/windows/text.h new file mode 100644 index 0000000..09c8f35 --- /dev/null +++ b/src/windows/text.h @@ -0,0 +1,48 @@ +// UTF-16, the system's text, from the UTF-8 this package passes names in (R7), +// and back. Included after `import std' by the units that need it. +#pragma once + +namespace { + +inline std::u16string utf16_from_utf8(std::string_view utf8) { + if (utf8.empty()) return {}; + const int n = MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + std::u16string out(static_cast(n > 0 ? n : 0), u'\0'); + if (n > 0) MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), out.data(), n); + return out; +} + +inline std::u16string utf16_from_path(const std::filesystem::path& name) { return utf16_from_utf8(mcpplibs::os::fs::utf8(name)); } + +// A `wchar_t' string is UTF-16 under the system's C environment and UTF-32 +// under one that presents POSIX. +inline std::u16string utf16_from_wide(std::wstring_view wide) { + if constexpr (sizeof(wchar_t) == 2) { + return std::u16string(wide.begin(), wide.end()); + } else { + std::u16string out; + for (const wchar_t w : wide) { + const auto c = static_cast(w); + if (c < 0x10000) { + out.push_back(static_cast(c)); + } else { + out.push_back(static_cast(0xD800 + ((c - 0x10000) >> 10))); + out.push_back(static_cast(0xDC00 + ((c - 0x10000) & 0x3FF))); + } + } + return out; + } +} + +inline std::string utf8_from_utf16(std::u16string_view text) { + if (text.empty()) return {}; + const int n = WideCharToMultiByte(65001, 0, text.data(), static_cast(text.size()), nullptr, 0, nullptr, nullptr); + std::string out(static_cast(n > 0 ? n : 0), '\0'); + if (n > 0) WideCharToMultiByte(65001, 0, text.data(), static_cast(text.size()), out.data(), n, nullptr, nullptr); + return out; +} + +// A name the system returned, as a path. +inline std::filesystem::path path_from_utf16(std::u16string_view text) { return mcpplibs::os::fs::from_utf8(utf8_from_utf16(text)); } + +} // namespace diff --git a/src/windows/win32.h b/src/windows/win32.h index 217692e..7a496c4 100644 --- a/src/windows/win32.h +++ b/src/windows/win32.h @@ -2,12 +2,29 @@ // than taken from (part 2 §6.2): the units then compile the same // under the MSVC ABI and under any C environment, and what they reach of the // system is this list. +// +// THE TYPES ARE THE SYSTEM'S AND NOT THE C ENVIRONMENT'S. A C library that +// presents POSIX on Windows (openkal-musl, mcpp docs/24) brings the LP64 data +// model and a 32-bit `wchar_t', so `unsigned long' and `wchar_t' are not +// DWORD and WCHAR there. A DWORD is written `okos_dword' (32 bits) and a +// WCHAR `char16_t' (UTF-16) in every environment. +// +// TWO KINDS OF BINDING. The names declared `dllimport' are bound when the +// program is linked, and they are names every program over openkal links: +// openkal-windows calls each of them itself, so the import library of any +// toolchain --- the system's, or the one openkal-windows generates from its own +// list where the system's is absent --- carries them. The names in `late' are +// kernel32's too, and are found when first called (LoadLibraryW and +// GetProcAddress), so that a toolchain without the system's import libraries +// links this package unchanged. advapi32 is reached the same way, so that a +// program links nothing beyond kernel32 for a question few programs ask. #pragma once extern "C" { using okos_bool = int; -using okos_dword = unsigned long; +using okos_dword = unsigned int; using okos_handle = void*; +using okos_wchar = char16_t; struct okos_memory_status { okos_dword length, load; @@ -19,43 +36,92 @@ struct okos_persistent_volume { }; struct okos_token_elevation { okos_dword elevated; }; -__declspec(dllimport) okos_bool __stdcall GlobalMemoryStatusEx(okos_memory_status*); -__declspec(dllimport) okos_handle __stdcall CreateFileW(const wchar_t*, okos_dword, okos_dword, void*, okos_dword, okos_dword, okos_handle); +__declspec(dllimport) okos_handle __stdcall CreateFileW(const okos_wchar*, okos_dword, okos_dword, void*, okos_dword, okos_dword, okos_handle); __declspec(dllimport) okos_bool __stdcall DeviceIoControl(okos_handle, okos_dword, void*, okos_dword, void*, okos_dword, okos_dword*, void*); __declspec(dllimport) okos_bool __stdcall CloseHandle(okos_handle); __declspec(dllimport) okos_handle __stdcall GetCurrentProcess(void); -__declspec(dllimport) okos_bool __stdcall IsWow64Process2(okos_handle, unsigned short*, unsigned short*); -// advapi32 is reached at run time, so that a program linking this package -// links nothing beyond kernel32 for a question few programs ask. -__declspec(dllimport) okos_handle __stdcall LoadLibraryW(const wchar_t*); +__declspec(dllimport) okos_handle __stdcall LoadLibraryW(const okos_wchar*); __declspec(dllimport) void* __stdcall GetProcAddress(okos_handle, const char*); -// mcpplibs.os.library. -__declspec(dllimport) okos_bool __stdcall FreeLibrary(okos_handle); -__declspec(dllimport) okos_handle __stdcall GetModuleHandleW(const wchar_t*); -__declspec(dllimport) okos_dword __stdcall GetLastError(void); -__declspec(dllimport) okos_bool __stdcall K32EnumProcessModules(okos_handle, okos_handle*, okos_dword, okos_dword*); -__declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned, okos_dword, const char*, int, wchar_t*, int); -// 0.2.0 __declspec(dllimport) okos_dword __stdcall GetLastError(void); +__declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned, okos_dword, const char*, int, okos_wchar*, int); +__declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned, okos_dword, const okos_wchar*, int, char*, int, + const char*, okos_bool*); __declspec(dllimport) okos_bool __stdcall FlushFileBuffers(okos_handle); -__declspec(dllimport) okos_bool __stdcall GetVolumePathNameW(const wchar_t*, wchar_t*, okos_dword); -__declspec(dllimport) okos_bool __stdcall GetVolumeInformationW(const wchar_t*, wchar_t*, okos_dword, okos_dword*, - okos_dword*, okos_dword*, wchar_t*, okos_dword); __declspec(dllimport) void* __stdcall LocalFree(void*); -__declspec(dllimport) okos_dword __stdcall GetActiveProcessorCount(unsigned short); -__declspec(dllimport) okos_bool __stdcall GetLogicalProcessorInformationEx(int, void*, okos_dword*); -__declspec(dllimport) unsigned __stdcall GetACP(void); -__declspec(dllimport) unsigned __stdcall GetConsoleOutputCP(void); __declspec(dllimport) okos_bool __stdcall GetConsoleMode(okos_handle, okos_dword*); -__declspec(dllimport) unsigned __stdcall GetDriveTypeW(const wchar_t*); -__declspec(dllimport) okos_dword __stdcall GetFileAttributesW(const wchar_t*); __declspec(dllimport) okos_dword __stdcall GetCurrentProcessId(void); -__declspec(dllimport) unsigned long long __stdcall GetTickCount64(void); __declspec(dllimport) okos_bool __stdcall SetHandleInformation(okos_handle, okos_dword, okos_dword); -// The C runtime's descriptors (every Windows C environment has these). -int __cdecl _dup(int); -int __cdecl _dup2(int, int); -int __cdecl _close(int); -long long __cdecl _get_osfhandle(int); -int __cdecl _write(int, const void*, unsigned); +// 0.2.1: the process's standard handles (STD_OUTPUT_HANDLE = -11, STD_ERROR_HANDLE = -12). +__declspec(dllimport) okos_handle __stdcall GetStdHandle(okos_dword); +__declspec(dllimport) okos_bool __stdcall GetHandleInformation(okos_handle, okos_dword*); +} + +namespace late { + +template +F find(const char* name) { + return reinterpret_cast(GetProcAddress(LoadLibraryW(u"kernel32.dll"), name)); +} + +inline okos_bool GlobalMemoryStatusEx(okos_memory_status* a0) { + static const auto f = find("GlobalMemoryStatusEx"); + return f(a0); +} +inline okos_bool IsWow64Process2(okos_handle a0, unsigned short* a1, unsigned short* a2) { + static const auto f = find("IsWow64Process2"); + return f(a0, a1, a2); +} +inline okos_bool FreeLibrary(okos_handle a0) { + static const auto f = find("FreeLibrary"); + return f(a0); +} +inline okos_handle GetModuleHandleW(const okos_wchar* a0) { + static const auto f = find("GetModuleHandleW"); + return f(a0); +} +inline okos_bool K32EnumProcessModules(okos_handle a0, okos_handle* a1, okos_dword a2, okos_dword* a3) { + static const auto f = find("K32EnumProcessModules"); + return f(a0, a1, a2, a3); +} +inline okos_bool GetVolumePathNameW(const okos_wchar* a0, okos_wchar* a1, okos_dword a2) { + static const auto f = find("GetVolumePathNameW"); + return f(a0, a1, a2); } +inline okos_bool GetVolumeInformationW(const okos_wchar* a0, okos_wchar* a1, okos_dword a2, okos_dword* a3, okos_dword* a4, okos_dword* a5, okos_wchar* a6, okos_dword a7) { + static const auto f = find("GetVolumeInformationW"); + return f(a0, a1, a2, a3, a4, a5, a6, a7); +} +inline okos_dword GetActiveProcessorCount(unsigned short a0) { + static const auto f = find("GetActiveProcessorCount"); + return f(a0); +} +inline okos_bool GetLogicalProcessorInformationEx(int a0, void* a1, okos_dword* a2) { + static const auto f = find("GetLogicalProcessorInformationEx"); + return f(a0, a1, a2); +} +inline unsigned GetACP() { + static const auto f = find("GetACP"); + return f(); +} +inline unsigned GetConsoleOutputCP() { + static const auto f = find("GetConsoleOutputCP"); + return f(); +} +inline unsigned GetDriveTypeW(const okos_wchar* a0) { + static const auto f = find("GetDriveTypeW"); + return f(a0); +} +inline okos_dword GetFileAttributesW(const okos_wchar* a0) { + static const auto f = find("GetFileAttributesW"); + return f(a0); +} +inline unsigned long long GetTickCount64() { + static const auto f = find("GetTickCount64"); + return f(); +} +inline okos_bool SetStdHandle(okos_dword a0, okos_handle a1) { + static const auto f = find("SetStdHandle"); + return f(a0, a1); +} + +} // namespace late diff --git a/src/windows/windows.cpp b/src/windows/windows.cpp index c4df701..4d76dca 100644 --- a/src/windows/windows.cpp +++ b/src/windows/windows.cpp @@ -6,12 +6,18 @@ module mcpplibs.os.windows; import std; import mcpplibs.os.fs; +#include "text.h" + namespace mcpplibs::os::windows { +namespace { +okos_handle invalid() { return reinterpret_cast(static_cast(-1)); } +} // namespace + bool elevated() { using open_token = okos_bool (__stdcall*)(okos_handle, okos_dword, okos_handle*); using token_information = okos_bool (__stdcall*)(okos_handle, int, void*, okos_dword, okos_dword*); - const okos_handle advapi = LoadLibraryW(L"advapi32.dll"); + const okos_handle advapi = LoadLibraryW(u"advapi32.dll"); if (!advapi) return false; const auto open = reinterpret_cast(GetProcAddress(advapi, "OpenProcessToken")); const auto query = reinterpret_cast(GetProcAddress(advapi, "GetTokenInformation")); @@ -26,14 +32,11 @@ bool elevated() { } std::optional on_dev_drive(const std::filesystem::path& directory) { - const std::string utf8 = fs::utf8(directory); - const int n = MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); - if (n <= 0) return std::nullopt; - std::wstring wide(static_cast(n), L'\0'); - MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), wide.data(), n); + const std::u16string wide = utf16_from_path(directory); + if (wide.empty()) return std::nullopt; const okos_handle h = CreateFileW(wide.c_str(), 0x0080 /* FILE_READ_ATTRIBUTES */, 0x7, nullptr, 3 /* OPEN_EXISTING */, 0x02000000 /* BACKUP_SEMANTICS */, nullptr); - if (h == reinterpret_cast(static_cast(-1)) || h == nullptr) return std::nullopt; + if (h == invalid() || h == nullptr) return std::nullopt; okos_persistent_volume info{}; info.version = 1; info.flag_mask = 0x00002000; // PERSISTENT_VOLUME_STATE_DEV_VOLUME @@ -45,19 +48,22 @@ std::optional on_dev_drive(const std::filesystem::path& directory) { return (info.volume_flags & 0x00002000) != 0; } -unsigned code_page() { return GetACP(); } +unsigned code_page() { return late::GetACP(); } +// The process's standard handle: what the stream writes to under every C +// environment, and what a started program receives. unsigned console_output_code_page(bool error_stream) { - const long long h = _get_osfhandle(error_stream ? 2 : 1); + const okos_handle h = GetStdHandle(static_cast(error_stream ? -12 : -11)); okos_dword mode = 0; - if (h == -1 || !GetConsoleMode(reinterpret_cast(h), &mode)) return 0; - return GetConsoleOutputCP(); + if (h == nullptr || h == invalid() || !GetConsoleMode(h, &mode)) return 0; + return late::GetConsoleOutputCP(); } namespace { -using reg_get_value = long (__stdcall*)(void*, const wchar_t*, const wchar_t*, okos_dword, okos_dword*, void*, okos_dword*); +using reg_get_value = int (__stdcall*)(void*, const okos_wchar*, const okos_wchar*, okos_dword, okos_dword*, void*, + okos_dword*); reg_get_value registry_reader() { - const okos_handle advapi = LoadLibraryW(L"advapi32.dll"); + const okos_handle advapi = LoadLibraryW(u"advapi32.dll"); return advapi ? reinterpret_cast(GetProcAddress(advapi, "RegGetValueW")) : nullptr; } void* const kLocalMachine = reinterpret_cast(static_cast(static_cast(0x80000002))); @@ -67,7 +73,7 @@ bool long_paths_enabled() { const auto get = registry_reader(); if (!get) return false; okos_dword value = 0, bytes = sizeof value; - return get(kLocalMachine, L"SYSTEM\\CurrentControlSet\\Control\\FileSystem", L"LongPathsEnabled", + return get(kLocalMachine, u"SYSTEM\\CurrentControlSet\\Control\\FileSystem", u"LongPathsEnabled", 0x10 /* RRF_RT_REG_DWORD */, nullptr, &value, &bytes) == 0 && value == 1; } @@ -75,36 +81,36 @@ bool long_paths_enabled() { std::optional machine_registry_path(std::wstring_view key, std::wstring_view value) { const auto get = registry_reader(); if (!get) return std::nullopt; - const std::wstring k(key), v(value); + const std::u16string k = utf16_from_wide(key), v = utf16_from_wide(value); for (const okos_dword view : { okos_dword{ 0x0100 } /* KEY_WOW64_64KEY */, okos_dword{ 0x0200 } /* 32KEY */ }) { okos_dword bytes = 0; if (get(kLocalMachine, k.c_str(), v.c_str(), 0x2 /* RRF_RT_REG_SZ */ | view, nullptr, nullptr, &bytes) != 0 - || bytes < sizeof(wchar_t)) + || bytes < sizeof(okos_wchar)) continue; - std::wstring buf(bytes / sizeof(wchar_t), L'\0'); + std::u16string buf(bytes / sizeof(okos_wchar), u'\0'); if (get(kLocalMachine, k.c_str(), v.c_str(), 0x2 | view, nullptr, buf.data(), &bytes) != 0) continue; - while (!buf.empty() && (buf.back() == L'\0' || buf.back() == L'\\')) buf.pop_back(); - if (!buf.empty()) return std::filesystem::path(buf); + while (!buf.empty() && (buf.back() == u'\0' || buf.back() == u'\\')) buf.pop_back(); + if (!buf.empty()) return path_from_utf16(buf); } return std::nullopt; } bool writable_fixed_drive(char drive, std::wstring_view child) { - const wchar_t root[] = { static_cast(drive), L':', L'\\', L'\0' }; - if (GetDriveTypeW(root) != 3 /* DRIVE_FIXED */) return false; - const auto candidate = std::filesystem::path(root) / child; - const auto attributes = GetFileAttributesW(candidate.c_str()); + const std::u16string root = { static_cast(drive), u':', u'\\' }; + if (late::GetDriveTypeW(root.c_str()) != 3 /* DRIVE_FIXED */) return false; + const std::u16string candidate = root + utf16_from_wide(child); + const auto attributes = late::GetFileAttributesW(candidate.c_str()); if (attributes == 0xFFFFFFFFu) { const auto e = GetLastError(); if (e != 2 /* FILE_NOT_FOUND */ && e != 3 /* PATH_NOT_FOUND */) return false; } else if (!(attributes & 0x10 /* DIRECTORY */) || (attributes & 0x400 /* REPARSE_POINT */)) { return false; } - const auto probe = std::filesystem::path(root) - / std::format(".os-write-{}-{}", GetCurrentProcessId(), GetTickCount64()); + const std::u16string probe = + root + utf16_from_utf8(std::format(".os-write-{}-{}", GetCurrentProcessId(), late::GetTickCount64())); const okos_handle h = CreateFileW(probe.c_str(), 0x40000000 /* GENERIC_WRITE */, 0, nullptr, 1 /* CREATE_NEW */, 0x100 | 0x04000000 /* TEMPORARY | DELETE_ON_CLOSE */, nullptr); - if (h == reinterpret_cast(static_cast(-1)) || h == nullptr) return false; + if (h == invalid() || h == nullptr) return false; CloseHandle(h); return true; } From 0dcad7f4d0d5ad58348cc1b26f733d6be4c1fcb4 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 02:02:15 +0900 Subject: [PATCH 2/8] checks/hosted: the tests over openkal-musl and openkal-llvm-runtime, in CI on four systems Each test of the package is compiled in that arrangement by an include from checks/hosted/tests; CI checks that every test has its counterpart and runs the suite on Linux x86_64 (dev, release), Linux aarch64, macOS and Windows (dev, release). The tests assert that Linux names the machine as the processor and that a volume's kind is named, and make a file executable through os::fs::set_executable, which works over every C library. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 82 ++++++++++++++++++++++ checks/hosted/mcpp.toml | 21 ++++++ checks/hosted/tests/dirs_terminal_test.cpp | 6 ++ checks/hosted/tests/fs_test.cpp | 6 ++ checks/hosted/tests/ipc_test.cpp | 6 ++ checks/hosted/tests/library_test.cpp | 6 ++ checks/hosted/tests/linux_test.cpp | 6 ++ checks/hosted/tests/platform_test.cpp | 6 ++ checks/hosted/tests/process_test.cpp | 6 ++ tests/fs_test.cpp | 3 + tests/platform_test.cpp | 5 ++ tests/process_test.cpp | 2 +- 12 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 checks/hosted/mcpp.toml create mode 100644 checks/hosted/tests/dirs_terminal_test.cpp create mode 100644 checks/hosted/tests/fs_test.cpp create mode 100644 checks/hosted/tests/ipc_test.cpp create mode 100644 checks/hosted/tests/library_test.cpp create mode 100644 checks/hosted/tests/linux_test.cpp create mode 100644 checks/hosted/tests/platform_test.cpp create mode 100644 checks/hosted/tests/process_test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec3f39b..02883d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ name: CI # # tests every module behaves as stated, on every system and with every # compiler family a consumer builds it with (the native rows) +# hosted the same tests, the program's C library and C++ run-time being +# openkal-musl and openkal-llvm-runtime on the system's own triple +# (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) # @@ -119,6 +122,85 @@ jobs: grep -Eiq "undefined|unresolved" misuse.log || { cat misuse.log; echo "::error::the misuse failed for another reason than an undefined name"; exit 1; } echo " ok the misuse fails at the link, naming what is undefined" + # ── THE TESTS OVER A C LIBRARY ON OPENKAL ────────────────────────────────── + # + # The C library presents POSIX and openkal rather than the system: its headers + # are not the SDK's, on Windows its data model is LP64 and its `wchar_t' 32 + # bits, and its descriptors and signals are its own. The units beside openkal + # reach each system through the system's own interface, and these rows are + # where that is asserted. + hosted: + name: hosted ${{ matrix.os }} ${{ matrix.profile }} + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, profile: dev } + - { os: ubuntu-24.04, profile: release } + - { os: ubuntu-24.04-arm, profile: dev } + - { os: macos-15, profile: dev } + - { os: windows-2022, profile: dev } + - { os: windows-2022, profile: release } + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + + - name: Install xlings and mcpp (Unix) + if: runner.os != 'Windows' + 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" + + - name: Install xlings and mcpp (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://d2learn.org/xlings-install.ps1.txt | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: The mcpp under cross-validation + run: | + 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 -o -name mcpp.exe \) | head -1) + [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } + echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" + echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" + fi + mcpp toolchain install llvm 23.1.3 + + - name: Every test of the package has its counterpart here + run: | + for t in tests/*_test.cpp; do + n=$(basename "$t") + [ -f "checks/hosted/tests/$n" ] || { echo "::error::checks/hosted/tests has no $n"; exit 1; } + done + + - name: Tests + working-directory: checks/hosted + run: | + profile='${{ matrix.profile }}' + mcpp test --profile "$profile" 2>&1 | tee tests.log + for name in platform_test fs_test process_test ipc_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 + # ── THE TESTS AS OK IMAGES (`-openkal`, openkal SPEC clause 12) ────── # # The same five suites, built once on Linux for both architectures and run diff --git a/checks/hosted/mcpp.toml b/checks/hosted/mcpp.toml new file mode 100644 index 0000000..4c0a819 --- /dev/null +++ b/checks/hosted/mcpp.toml @@ -0,0 +1,21 @@ +# The package's tests in the arrangement a program over openkal chooses on a +# system's own triple: the C library is openkal-musl and the C++ run-time is +# openkal-llvm-runtime, both above openkal's implementation for that system +# (mcpp docs/24; mcpp design 2026-10-28 §5.4). The units of this package that +# reach the system beside openkal then see musl's headers, POSIX's names and +# the data model and `wchar_t' the C library declares, not the system's SDK. +[package] +name = "os-check-hosted" +version = "0.1.0" + +[toolchain] +default = "llvm@23.1.3" + +[dependencies] +os = { path = "../.." } +openkal-musl = { version = "0.22.3", default-features = false, features = ["posix"] } +openkal-llvm-runtime = "0.16.3" + +# The tests are the package's own, each reached by an include from +# tests/.cpp here (a test program is a file of the package that runs it). +# CI checks that every test of the package has its counterpart. diff --git a/checks/hosted/tests/dirs_terminal_test.cpp b/checks/hosted/tests/dirs_terminal_test.cpp new file mode 100644 index 0000000..1a50a0f --- /dev/null +++ b/checks/hosted/tests/dirs_terminal_test.cpp @@ -0,0 +1,6 @@ +// dirs_terminal_test.cpp of mcpplibs.os, compiled in this arrangement. 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/hosted/tests/fs_test.cpp b/checks/hosted/tests/fs_test.cpp new file mode 100644 index 0000000..8560ba7 --- /dev/null +++ b/checks/hosted/tests/fs_test.cpp @@ -0,0 +1,6 @@ +// fs_test.cpp of mcpplibs.os, compiled in this arrangement. 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/hosted/tests/ipc_test.cpp b/checks/hosted/tests/ipc_test.cpp new file mode 100644 index 0000000..7d11adb --- /dev/null +++ b/checks/hosted/tests/ipc_test.cpp @@ -0,0 +1,6 @@ +// ipc_test.cpp of mcpplibs.os, compiled in this arrangement. 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/ipc_test.cpp" diff --git a/checks/hosted/tests/library_test.cpp b/checks/hosted/tests/library_test.cpp new file mode 100644 index 0000000..9bfc51d --- /dev/null +++ b/checks/hosted/tests/library_test.cpp @@ -0,0 +1,6 @@ +// library_test.cpp of mcpplibs.os, compiled in this arrangement. 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/hosted/tests/linux_test.cpp b/checks/hosted/tests/linux_test.cpp new file mode 100644 index 0000000..4022ab8 --- /dev/null +++ b/checks/hosted/tests/linux_test.cpp @@ -0,0 +1,6 @@ +// linux_test.cpp of mcpplibs.os, compiled in this arrangement. 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/hosted/tests/platform_test.cpp b/checks/hosted/tests/platform_test.cpp new file mode 100644 index 0000000..4ec5c7d --- /dev/null +++ b/checks/hosted/tests/platform_test.cpp @@ -0,0 +1,6 @@ +// platform_test.cpp of mcpplibs.os, compiled in this arrangement. 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/checks/hosted/tests/process_test.cpp b/checks/hosted/tests/process_test.cpp new file mode 100644 index 0000000..002e493 --- /dev/null +++ b/checks/hosted/tests/process_test.cpp @@ -0,0 +1,6 @@ +// process_test.cpp of mcpplibs.os, compiled in this arrangement. 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/process_test.cpp" diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index d9eec8e..cada765 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -130,6 +130,9 @@ int main() { "a file and a directory are made durable"); const auto b = os::fs::measure(dir); check(b.measured, "a writable directory's volume is measured"); + // The kind of a volume is the system's to name; an image is not told it. + if constexpr (!os::is_image) + check(!b.type.empty() && b.type != "0x0", "and its kind is named by the system"); (void)os::fs::write(dir / "x", "x"); check(os::fs::place(dir / "x", dir / "y").has_value() && os::fs::read(dir / "y").value_or("") == "x" && !std::filesystem::exists(dir / "x"), diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp index b337163..51dccaa 100644 --- a/tests/platform_test.cpp +++ b/tests/platform_test.cpp @@ -22,6 +22,11 @@ int main() { const auto system = os::env::system(); check(system && *system == os::name(host), "openkal names the system the program runs upon"); check(!os::machine::arch().empty(), "the machine's architecture is named"); + // Linux translates no program to another processor's instructions: the + // machine is named as the processor this program runs on, whichever C + // 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) 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); diff --git a/tests/process_test.cpp b/tests/process_test.cpp index 1aa093a..f074ff5 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -253,7 +253,7 @@ int main(int argc, char** argv) { // program is replaced. const auto file = std::filesystem::temp_directory_path() / "os-not-a-program"; { std::ofstream(file) << "this is not an executable format\n"; } - std::filesystem::permissions(file, std::filesystem::perms::owner_all); + check(os::fs::set_executable(file, true).has_value(), "the file is made executable"); os::process::spawn_options o; o.program = file; o.in = o.out = o.err = os::process::stdio::inherit; From 233070e4d3ffe97453a1e249b75d05edf59cb681 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 02:02:15 +0900 Subject: [PATCH 3/8] 0.2.1: os over the program's own C library on every host Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 30 ++++++++++++++++++--- 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7b003ef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +## 0.2.1 + +The package builds and its tests pass in every arrangement of the program's C +library on Linux, macOS and Windows: the system's own (glibc, libSystem, the +Microsoft C runtime), and a C library that sits on openkal (openkal-musl with +openkal-llvm-runtime, mcpp docs/24) on the system's own triple. Under the +second, a C library presents POSIX and openkal, not the system: its headers are +not the SDK's, on Windows its data model is LP64 and its `wchar_t` 32 bits, and +its descriptors, signals, `uname`, `sysinfo` and `syscall` are its own. + +- **The units beside openkal reach each system through that system's stable + interface, not through the C library.** + - **Linux:** the kernel's calls, one trap per architecture + (`src/linux/kernel.h`, `src/linux/arch/`). These cover the machine's name + and memory, a volume's kind and an exchange of two entries. In the second + arrangement the C library had answered `openkal` for the machine, had no + `sysinfo`, and turned `renameat2(RENAME_EXCHANGE)` into a plain rename that + replaced the second entry. + - **macOS:** the kernel's calls for a volume's kind and an exchange + (`src/macos/kernel.h`, as openkal-macos itself does), and the libSystem + names no C library defines for sysctl and the host's statistics + (`src/macos/darwin.h`). These are declared rather than included from the + SDK. A link whose libSystem is openkal-macos's stub binds them when the + program is loaded (`-U`). + - **Windows:** `src/windows/win32.h` declares DWORD as 32 bits and WCHAR as + UTF-16 (`char16_t`) under every C environment. Names reach the system as + UTF-8 converted to UTF-16, never through `path::c_str()` (R7). The + kernel32 names that openkal-windows does not itself call are found when + first called, so a toolchain without the system's import libraries links + the package. +- **The process's output and its ending for a closed reader are changed where + the process keeps them.** + - On Linux and macOS, the diversion of the standard output (`output_to_error`), + the reset of SIGPIPE before `exec` and `end_as_output_closed` use the + kernel's descriptors and dispositions (`src/posix/kernel.h`): what openkal's + standard output, the C library's and a started program all use. + - On Windows the diversion moves the Microsoft C runtime's descriptor, as + before (`src/windows/crt/stdio.cpp`). For a C library on openkal it moves + that library's descriptor and the process's standard handle, each apart + (`src/windows/posix/stdio.cpp`). The manifest selects the form by + `cfg(c-abi = "musl")`. +- **`checks/hosted`** builds the package's tests over openkal-musl and + openkal-llvm-runtime. CI runs them on Linux, macOS and Windows, beside the + native rows and the ok images. Locally, the native and hosted suites pass on + Linux x86_64 (gcc 16.1.0 and llvm 23.1.3, dev and release) and on aarch64 + under qemu. The Windows and macOS builds are cross-linked from Linux. +- Tests: `platform_test` asserts that Linux names the machine as the processor + the program runs on. `fs_test` asserts that a volume's kind is named. + `process_test` makes a file executable with `os::fs::set_executable` rather + than with `std::filesystem::permissions`, which a C library on openkal + refuses. + +## 0.2.0 + +What mcpp's operating-system layer carried: `exec`; both outputs on one pipe; +appending and durability; measured volumes; place and exchange; owners, +processors and available memory; a bounded read of input; output sent to the +error stream; raw mode that keeps the interrupt; Windows facts. Over openkal +0.17.1 (openkal-linux 0.18.2, openkal-macos 0.15.1, openkal-windows 0.13.3); ok +images over openkal-llvm-runtime 0.16.3 and openkal-tools 0.1.1 (#3). + +## 0.1.2 + +Shared libraries at run time (`mcpplibs.os.library`) (#5). + +## 0.1.1 + +Names reach the system as UTF-8 and come back as UTF-8 on every host, never +through the process's code page (R7) (#4). + +## 0.1.0 + +A C++ operating-system library over openkal 0.17 (#1, #2). diff --git a/README.md b/README.md index 3792361..faf132e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ with compile-time platform constants and a few platform modules. ```toml [dependencies] -os = { namespace = "mcpplibs", version = "0.2.0" } +os = { namespace = "mcpplibs", version = "0.2.1" } ``` ```cpp @@ -34,6 +34,28 @@ what is not a kernel question: the machine beneath the system (`machine`), the d loader (`library`: the C library's on Linux and macOS, kernel32's on Windows), and the platform modules. +## The program's C library + +A program over this package chooses its C library. It can be the system's own (glibc, +libSystem, the Microsoft C runtime), or one that sits on openkal on the system's own +triple: openkal-musl with openkal-llvm-runtime (mcpp `docs/24`). The package behaves the +same under either. + +Native code beside openkal reaches each system through that system's stable interface, +never through the C library, because a C library on openkal presents POSIX and openkal +rather than the system: + +| System | How native code reaches it | +|---|---| +| Linux | the kernel's calls, one trap per architecture (`src/linux/kernel.h`) | +| macOS | the kernel's calls (`src/macos/kernel.h`), and the libSystem names no C library defines (`src/macos/darwin.h`) | +| Windows | kernel32 and advapi32, with DWORD as 32 bits and WCHAR as UTF-16 under every C environment (`src/windows/win32.h`) | + +The standard output's diversion and the ending for a closed reader act on the process +itself: the kernel's descriptors and dispositions, or on Windows the process's standard +handles. `checks/hosted` runs the tests under openkal-musl and openkal-llvm-runtime on +every system in CI. + ## Modules | Module | Contents | @@ -77,8 +99,10 @@ platform modules. nothing else. The tests write, read and rename names in four scripts. Interface units import `std` and contain no system header and no macro. Implementation units -on openkal import only `openkal.*`. The Windows units declare the functions they call -themselves (`src/windows/win32.h`). +on openkal import only `openkal.*`. The Windows and macOS units declare the functions they +call themselves (`src/windows/win32.h`, `src/macos/darwin.h`). Linux's and macOS's kernel +calls are in `src/linux/arch/` and `src/macos/arch/`, which the manifest selects by +architecture. ## As an ok image From 8e0d950b06149ff2f5091750256baa730c76bb4c Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 02:13:35 +0900 Subject: [PATCH 4/8] ci(hosted): the system's own triple named on macOS and Windows, as a program in this arrangement names it Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02883d2..c4466e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,13 +136,16 @@ jobs: strategy: fail-fast: false matrix: + # The system's own triple; on macOS and Windows named, as a program in + # this arrangement names it: a Windows host defaults to the MSVC target, + # and openkal's Windows implementation is reached through the GNU one. include: - - { os: ubuntu-24.04, profile: dev } - - { os: ubuntu-24.04, profile: release } - - { os: ubuntu-24.04-arm, profile: dev } - - { os: macos-15, profile: dev } - - { os: windows-2022, profile: dev } - - { os: windows-2022, profile: release } + - { os: ubuntu-24.04, target: '', profile: dev } + - { os: ubuntu-24.04, target: '', profile: release } + - { os: ubuntu-24.04-arm, target: '', profile: dev } + - { os: macos-15, target: '--target aarch64-macos', profile: dev } + - { os: windows-2022, target: '--target x86_64-windows-gnu', profile: dev } + - { os: windows-2022, target: '--target x86_64-windows-gnu', profile: release } defaults: run: shell: bash @@ -196,7 +199,7 @@ jobs: working-directory: checks/hosted run: | profile='${{ matrix.profile }}' - mcpp test --profile "$profile" 2>&1 | tee tests.log + mcpp test --profile "$profile" ${{ matrix.target }} 2>&1 | tee tests.log for name in platform_test fs_test process_test ipc_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 From ba59bb292f2f26631a0c2217cccfc9acfe8eedd1 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 03:14:44 +0900 Subject: [PATCH 5/8] what mcpp-language-server's operating-system layer carried: bounded runs, threads, environment entries, a bounded read, canonical names mcppls removes its modules/os and modules/platform in 0.0.18 and calls mcpplibs.os directly; what it needed and os did not have is here: - process::run(options, bounds, input): a hard bound, a grace, a drain, and per-output limits; it says whether the program was ended at the bound, whether a descendant held an output open (and ends the unit), and which output it cut. - process::threads(id): each thread of a program with its processor time (Linux /proc; none elsewhere). - env::assignments(entries): `NAME=value` entries as spawn_options overrides. - fs::read(file, at_most): errc::too_large past the bound (new condition, after timed_out). - fs::canonical(name): links followed and, on Windows, short components replaced by their long names. Tests in process_test and fs_test; native and hosted (openkal-musl + openkal-llvm-runtime) pass on Linux x86_64, and the hosted package cross-builds for x86_64-windows-gnu and aarch64-macos. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 17 ++++- README.md | 6 +- src/env.cpp | 11 ++++ src/env.cppm | 7 +++ src/error.cpp | 1 + src/error.cppm | 1 + src/fs.cpp | 56 ++++++++++++++++- src/fs.cppm | 11 ++++ src/process.cpp | 137 +++++++++++++++++++++++++++++++++++++++++ src/process.cppm | 36 +++++++++++ tests/fs_test.cpp | 23 +++++++ tests/process_test.cpp | 55 +++++++++++++++++ 12 files changed, 355 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b003ef..0b2ad18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,11 +46,26 @@ its descriptors, signals, `uname`, `sysinfo` and `syscall` are its own. native rows and the ok images. Locally, the native and hosted suites pass on Linux x86_64 (gcc 16.1.0 and llvm 23.1.3, dev and release) and on aarch64 under qemu. The Windows and macOS builds are cross-linked from Linux. +- **What mcpp-language-server's operating-system layer carried** (its + `modules/os` and `modules/platform`, removed in its 0.0.18): + - `process::run(options, bounds, input)`: a run with a hard bound, a grace + and a drain, and limits on what each output keeps. It reports a timeout, + an output a descendant held open, and an output it cut. + - `process::threads(id)`: the threads of a program and the processor time of + each (Linux; none elsewhere). + - `env::assignments(entries)`: `NAME=value` entries as the overrides of a + started program's environment. + - `fs::read(file, at_most)`: a bounded read, `errc::too_large` past the + bound (new error condition). + - `fs::canonical(name)`: the name a node is known by, with links followed + and, on Windows, short (8.3) components replaced by their long names. - Tests: `platform_test` asserts that Linux names the machine as the processor the program runs on. `fs_test` asserts that a volume's kind is named. `process_test` makes a file executable with `os::fs::set_executable` rather than with `std::filesystem::permissions`, which a C library on openkal - refuses. + refuses. `process_test` covers the bounded run (a cut output, a hard bound, + an output held open), the threads of this program and environment entries; + `fs_test` covers the bounded read and the canonical name. ## 0.2.0 diff --git a/README.md b/README.md index faf132e..c98e320 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,9 @@ every system in CI. |---|---| | `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.error` | `errc` (openkal's closed set of conditions), `error`, `result` | -| `mcpplibs.os.env` | variables (names without case on Windows), 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`; 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` | -| `mcpplibs.os.process` | `spawn`, `run`, `exec`, a command line joined as it is for the command interpreter (`one_string`), pipes, both outputs on one pipe, output onto this program's error output, units and `end_unit`, bound lifetime, bounded waits and reads; `self`, `describe`, `alive` | +| `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` | +| `mcpplibs.os.process` | `spawn`, `run` (and with bounds: a hard bound, a grace, a drain, output limits), `exec`, a command line joined as it is for the command interpreter (`one_string`), pipes, both outputs on one pipe, output onto this program's error output, units and `end_unit`, bound lifetime, bounded waits and reads; `self`, `describe`, `alive`, `threads` | | `mcpplibs.os.interrupt` | `requested`, `wait`, `end_as_requested`, `end_as_output_closed` | | `mcpplibs.os.stdio` | the three streams as bytes; a bounded read of input; the output sent to the error stream for a while (`output_to_error`) | | `mcpplibs.os.terminal` | `interactive`, `size`, `sequences`, `color`, `raw_mode` (with or without the interrupting keystroke), `foreground` | diff --git a/src/env.cpp b/src/env.cpp index d091bbc..5153360 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -37,6 +37,17 @@ std::vector variables() { return out; } +std::vector>> assignments(std::span entries) { + std::vector>> out; + out.reserve(entries.size()); + for (const auto& entry : entries) { + const auto equals = entry.find('=', 1); + if (equals == std::string::npos) continue; + out.emplace_back(entry.substr(0, equals), entry.substr(equals + 1)); + } + return out; +} + std::vector arguments() { std::vector out; const kal_uintptr n = kal_env_arg_count(); diff --git a/src/env.cppm b/src/env.cppm index a5e9655..660555f 100644 --- a/src/env.cppm +++ b/src/env.cppm @@ -18,6 +18,13 @@ std::optional get(std::string_view name); // Every variable, as `NAME=value', in an order that does not change. std::vector variables(); +// `NAME=value' entries --- as `variables' gives them, or as a program states +// an environment of its own --- as the overrides `process::spawn_options' +// states a started program's environment in (0.2.1). An entry with no `=' +// after its first character is left out: on Windows a variable may begin +// with one (`=C:'). +std::vector>> assignments(std::span entries); + // The arguments, the first being the name the program was started by. std::vector arguments(); diff --git a/src/error.cpp b/src/error.cpp index 9e8f6f2..92a6efe 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -21,6 +21,7 @@ std::string_view describe(errc code) noexcept { case errc::not_directory: return "not a directory"; case errc::not_program: return "not a program this system can start"; case errc::timed_out: return "timed out"; + case errc::too_large: return "larger than the bound"; } return "unknown condition"; } diff --git a/src/error.cppm b/src/error.cppm index 3e3ce3b..8f4e860 100644 --- a/src/error.cppm +++ b/src/error.cppm @@ -13,6 +13,7 @@ enum class errc { invalid = 1, again, io, no_memory, no_space, permission, not_supported, closed, not_found, exists, not_empty, is_directory, not_directory, not_program, timed_out, + too_large, // more than the caller would take (0.2.1) }; struct error { diff --git a/src/fs.cpp b/src/fs.cpp index 3f6a215..7cb6cb1 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -240,7 +240,10 @@ std::vector preopens() { return names; } -result read(const std::filesystem::path& file) { +namespace { +// The whole file, or past `at_most' errc::too_large: each read asks for no more +// than one byte beyond the bound. +result read_up_to(const std::filesystem::path& file, std::optional at_most) { auto l = where(file, "reading"); if (!l) return std::unexpected(l.error()); opened_file f; @@ -250,13 +253,22 @@ result read(const std::filesystem::path& file) { std::string out; char buf[65536]; for (;;) { - const kal_intptr n = kal_stream_read(kal_fs_stream(f.f), buf, sizeof buf); + std::size_t want = sizeof buf; + 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); 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) }); } return out; } +} // namespace + +result read(const std::filesystem::path& file) { return read_up_to(file, std::nullopt); } + +result read(const std::filesystem::path& file, std::size_t at_most) { return read_up_to(file, at_most); } result write(const std::filesystem::path& file, std::string_view bytes) { auto l = where(file, "writing"); @@ -461,6 +473,46 @@ result identity_of(const std::filesystem::path& node) { return identity{ i->identity[0], i->identity[1] }; } +std::filesystem::path canonical(const std::filesystem::path& name) { + const std::string whole = generic(fs::absolute(name)); + if (host() == platform::windows) { + // A short alias always carries a `~'. A component that does is looked + // up among its parent's entries, which are listed by their long names, + // and replaced by the entry that is the same node. + const std::size_t rootLen = root_length(whole, true); + std::string current = whole.substr(0, rootLen); + std::string_view rest = std::string_view(whole).substr(rootLen); + while (!rest.empty()) { + const auto slash = rest.find('/'); + const std::string part(rest.substr(0, slash)); + rest = slash == std::string_view::npos ? std::string_view{} : rest.substr(slash + 1); + if (part.empty()) continue; + const std::string joint = current.empty() || current.ends_with('/') ? "" : "/"; + std::string next = current + joint + part; + if (part.find('~') != std::string::npos) { + if (const auto id = identity_of(from_utf8(next))) { + std::error_code ec; + for (std::filesystem::directory_iterator it(from_utf8(current), ec), end; !ec && it != end; it.increment(ec)) { + const std::string entry = generic_utf8(it->path().filename()); + if (entry.find('~') != std::string::npos) continue; + if (const auto other = identity_of(it->path()); other && *other == *id) { + next = current + joint + entry; + break; + } + } + } + } + current = std::move(next); + } + return from_utf8(current); + } + // Elsewhere the part that exists has its links followed and the rest is kept. + std::error_code ec; + const auto resolved = std::filesystem::weakly_canonical(from_utf8(whole), ec); + if (ec || resolved.empty()) return from_utf8(whole); + return from_utf8(generic(resolved)); +} + result rename(const std::filesystem::path& from, const std::filesystem::path& to, bool replace) { const std::string doing = std::format("renaming {} to {}", generic_utf8(from), generic_utf8(to)); auto a = where(from, "renaming"); diff --git a/src/fs.cppm b/src/fs.cppm index c92730f..99495df 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -67,6 +67,12 @@ bool is_absolute(const std::filesystem::path& name); bool same(const std::filesystem::path& a, const std::filesystem::path& b); bool within(const std::filesystem::path& name, const std::filesystem::path& directory); +// THE NAME A NODE IS KNOWN BY, ASKING THE FILE SYSTEM (0.2.1): the absolute +// name with every link followed and, on Windows, every short (8.3) component +// replaced by its long name, so that two names of one node are one text. The +// part that does not exist is kept as written. In the generic spelling. +std::filesystem::path canonical(const std::filesystem::path& name); + // Whether a name says where a file is, rather than naming one to look for on a // list of directories: it holds a separator in the host's spelling (on Windows // `\' as well as `/', or a drive). Asked of `host()' for the reason above. @@ -76,6 +82,11 @@ bool names_a_place(std::string_view name); result read(const std::filesystem::path& file); +// AT MOST `at_most' BYTES (0.2.1): errc::too_large when the file holds more, +// having read no more than one byte past the bound --- also of a file that +// grows while it is read. +result read(const std::filesystem::path& file, std::size_t at_most); + // Written in place: created, or truncated and rewritten. result write(const std::filesystem::path& file, std::string_view bytes); diff --git a/src/process.cpp b/src/process.cpp index 5979e28..f4658c2 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -396,6 +396,106 @@ result run(spawn_options options, std::string_view input) { return out; } +namespace { +// One output of a bounded run, read by a thread of its own in slices, so that +// the run can stop it once the program has ended and the drain has passed. +struct capture { + std::mutex lock; + std::string data; + std::size_t limit = 0; + bool truncated = false; +}; +struct collection { + std::mutex lock; + std::condition_variable changed; + int open = 2; + std::atomic stop{ false }; +}; +constexpr std::chrono::milliseconds read_slice{ 50 }; + +void collect(std::shared_ptr p, std::shared_ptr into, + std::shared_ptr all, bool output) { + while (!all->stop.load(std::memory_order_relaxed)) { + auto chunk = output ? p->read_output_for(read_slice) : p->read_error_for(read_slice); + if (!chunk) break; // the stream failed: nothing more arrives + if (!chunk->has_value()) continue; + if ((*chunk)->empty()) break; // its end + std::lock_guard g(into->lock); + if (into->data.size() >= into->limit) { into->truncated = true; continue; } + const std::size_t room = into->limit - into->data.size(); + if ((*chunk)->size() > room) { + into->data.append((*chunk)->data(), room); + into->truncated = true; + } else { + into->data += **chunk; + } + } + { + std::lock_guard g(all->lock); + --all->open; + } + all->changed.notify_all(); +} +} // namespace + +result run(spawn_options options, const bounds& limits, std::string_view input) { + options.in = stdio::pipe; + options.out = stdio::pipe; + options.err = stdio::pipe; + options.own_unit = true; + const auto started = std::chrono::steady_clock::now(); + auto spawned = process::spawn(options); + if (!spawned) return std::unexpected(spawned.error()); + auto p = std::make_shared(std::move(*spawned)); + if (!input.empty()) (void)p->write(input); + p->close_input(); + + auto out = std::make_shared(); + out->limit = limits.out_limit; + auto err = std::make_shared(); + err->limit = limits.err_limit; + auto all = std::make_shared(); + std::thread(collect, p, out, all, true).detach(); + std::thread(collect, p, err, all, false).detach(); + + bounded_output result; + auto waited = p->wait_for(limits.hard); + if (!waited) return std::unexpected(waited.error()); + if (!waited->has_value()) { + result.timed_out = true; + p->terminate(); // asked + (void)p->wait_for(limits.grace); + p->end_unit(); + auto ended = p->wait(); + result.status = ended ? *ended : exit_status{ -1, true }; + } else { + result.status = **waited; + } + { + std::unique_lock g(all->lock); + all->changed.wait_for(g, limits.drain, [&] { return all->open == 0; }); + result.held_open = all->open != 0; + } + if (result.held_open) { + p->end_unit(); // the program is gone; its unit is not + all->stop.store(true, std::memory_order_relaxed); + std::unique_lock g(all->lock); + all->changed.wait_for(g, read_slice * 4, [&] { return all->open == 0; }); + } + { + std::lock_guard g(out->lock); + result.out = out->data; + result.out_truncated = out->truncated; + } + { + std::lock_guard g(err->lock); + result.err = err->data; + result.err_truncated = err->truncated; + } + result.duration = std::chrono::duration_cast(std::chrono::steady_clock::now() - started); + return result; +} + result exec(spawn_options o) { if (o.in != stdio::inherit || o.out != stdio::inherit || o.err != stdio::inherit) return std::unexpected(error{ errc::invalid, @@ -453,4 +553,41 @@ bool alive(const program& p) { return p.started_ns == 0 || d->who.started_ns == 0 || d->who.started_ns == p.started_ns; } +std::vector threads(std::uint64_t id) { + std::vector out; + if (host() != platform::linux || id == 0) return out; + // /proc//task//stat: ` () ...', utime and stime + // the 12th and 13th fields after the name, in ticks of USER_HZ (100). + constexpr std::uint64_t ns_per_tick = 10'000'000; + const auto number = [](std::string_view text) -> std::optional { + std::uint64_t v = 0; + const auto [end, ec] = std::from_chars(text.data(), text.data() + text.size(), v); + if (ec != std::errc{} || end != text.data() + text.size()) return std::nullopt; + return v; + }; + std::error_code ec; + const std::filesystem::path tasks = std::format("/proc/{}/task", id); + for (std::filesystem::directory_iterator it(tasks, ec), end; !ec && it != end; it.increment(ec)) { + const auto stat = fs::read(it->path() / "stat"); + if (!stat) continue; + const auto open = stat->find('('); + const auto close = stat->rfind(')'); + if (open == std::string::npos || close == std::string::npos || close < open || close + 2 >= stat->size()) continue; + std::vector fields; + std::string_view rest = std::string_view(*stat).substr(close + 2); + while (!rest.empty()) { + const auto space = rest.find(' '); + if (const auto field = rest.substr(0, space); !field.empty()) fields.push_back(field); + rest = space == std::string_view::npos ? std::string_view{} : rest.substr(space + 1); + } + if (fields.size() < 13) continue; + const auto tid = number(std::string_view(*stat).substr(0, open > 0 ? open - 1 : 0)); + const auto user = number(fields[11]); + const auto system = number(fields[12]); + if (!tid || !user || !system) continue; + out.push_back(thread_info{ *tid, stat->substr(open + 1, close - open - 1), (*user + *system) * ns_per_tick }); + } + return out; +} + } // namespace mcpplibs::os::process diff --git a/src/process.cppm b/src/process.cppm index 9e61861..4af3723 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -118,6 +118,32 @@ struct output { }; result run(spawn_options options, std::string_view input = {}); +// A RUN WITH BOUNDS (0.2.1), for a program that may not end, may leave a +// descendant holding its output open, or may write without end. It runs in a +// unit of its own. Past `hard' it is asked to end and, `grace' later, its unit +// is ended. Its outputs are read for `drain' after it has ended; one a +// descendant still holds open then ends the unit as well (`held_open'). Each +// output keeps at most its limit, and what is past it is read and dropped, so +// the program never waits on a full pipe. +struct bounds { + std::chrono::milliseconds hard { std::chrono::seconds { 60 } }; + std::chrono::milliseconds grace { std::chrono::seconds { 2 } }; + std::chrono::milliseconds drain { std::chrono::seconds { 1 } }; + std::size_t out_limit = std::size_t { 256 } << 20; + std::size_t err_limit = std::size_t { 1 } << 20; +}; +struct bounded_output { + exit_status status; + std::string out; + std::string err; + bool timed_out = false; // `hard' passed and the program was ended + bool held_open = false; // an output stayed open after the program ended + bool out_truncated = false; + bool err_truncated = false; + std::chrono::milliseconds duration { 0 }; +}; +result run(spawn_options options, const bounds& limits, std::string_view input = {}); + // Becomes the program: returns only when it could not be started. Where the // system replaces a program's image (Linux, macOS) this program's image is // replaced, keeping its identity, its terminal and its place among the @@ -150,6 +176,16 @@ result describe(std::uint64_t id); // Whether that program is still the one that was named. bool alive(const program& p); +// THE THREADS OF A PROGRAM and the processor time each has used (0.2.1), where +// the system tells them apart (Linux: /proc//task); none elsewhere, where +// `describe' gives the program's whole time. +struct thread_info { + std::uint64_t id = 0; + std::string name; + std::uint64_t cpu_ns = 0; +}; +std::vector threads(std::uint64_t id); + } // namespace mcpplibs::os::process // Each system's own (src//exec.cpp): the image replaced by the program at diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index cada765..e6f9fd8 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -19,6 +19,29 @@ int main() { check(os::fs::read(file).value_or("") == "one", "and read back"); check(os::fs::write_atomic(file, "two").has_value(), "and replaced all at once"); check(os::fs::read(file).value_or("") == "two", "and the replacement is read"); + // 0.2.1: a read with a bound. + check(os::fs::read(file, 3).value_or("") == "two", "a file within the bound is read whole"); + { + const auto over = os::fs::read(file, 2); + check(!over && over.error().code == os::errc::too_large, "and one past it is refused as too large"); + } + // 0.2.1: the name a node is known by. + { + const auto canonical = os::fs::canonical(dir / "." / "sub" / ".." / file.filename()); + check(os::fs::same(canonical, os::fs::canonical(file)) && os::fs::is_absolute(canonical), + "a name and another of the same node have one canonical name"); + const auto missing = os::fs::canonical(dir / "not-there" / "deeper"); + check(os::fs::generic(missing).ends_with("not-there/deeper"), "and the part that does not exist is kept as written"); + if constexpr (os::is_posix) { + std::error_code ec; + stdfs::create_directory_symlink(dir, dir.parent_path() / std::format("{}-link", dir.filename().string()), ec); + if (!ec) { + const auto through = os::fs::canonical(dir.parent_path() / std::format("{}-link", dir.filename().string()) / file.filename()); + check(os::fs::same(through, os::fs::canonical(file)), "a link is followed"); + stdfs::remove(dir.parent_path() / std::format("{}-link", dir.filename().string()), ec); + } + } + } const auto caps = os::fs::caps(dir); check(caps.has_value(), "the volume says what it can do"); diff --git a/tests/process_test.cpp b/tests/process_test.cpp index f074ff5..2a63a59 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -122,6 +122,13 @@ int child(std::string_view errand) { std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; } + if (errand == "flood") { + // More output than a bounded run keeps. + const std::string block(4096, 'x'); + for (int i = 0; i < 64; ++i) (void)os::stdio::write_output(block); + (void)os::stdio::write_error("tail"); + return 0; + } if (errand == "interrupt") { (void)os::stdio::write_output(os::interrupt::available() ? "armed\n" : "none\n"); os::interrupt::wait(std::chrono::seconds(20)); @@ -192,6 +199,54 @@ int main(int argc, char** argv) { check(st && !st->success(), "and the unit, ended, does not report success"); } } + { + // 0.2.1: a run with bounds keeps at most its limits, reads the rest and drops it. + os::process::bounds limits; + limits.out_limit = 10000; + auto r = os::process::run(copy("flood"), limits); + check(r && r->status.success() && r->out.size() == 10000 && r->out_truncated && !r->timed_out, + "a bounded run keeps an output to its limit and says it cut it"); + check(r && r->err == "tail" && !r->err_truncated, "and keeps the other whole"); + } + { + // 0.2.1: past its hard bound a program is ended, and the run says so. + os::process::bounds limits; + limits.hard = std::chrono::milliseconds(300); + limits.grace = std::chrono::milliseconds(300); + const auto began = std::chrono::steady_clock::now(); + auto r = os::process::run(copy("sleep"), limits); + const auto took = std::chrono::steady_clock::now() - began; + check(r && r->timed_out && !r->status.success(), "a bounded run ends a program past its hard bound"); + check(took < std::chrono::seconds(15), "and does not wait for it to end by itself"); + } + { + // 0.2.1: a program that ends while what it left holds its output open. + auto o = copy("leave"); + o.in = os::process::stdio::null; + os::process::bounds limits; + limits.drain = std::chrono::milliseconds(300); + auto r = os::process::run(o, limits); + check(r && r->status.success() && r->held_open, "a bounded run says an output was held open, and ends the unit"); + } + if (os::host() == os::platform::linux && !os::is_image) { + // 0.2.1: the threads of a running program, each with its time. + const auto me = os::process::self(); + const auto threads = me ? os::process::threads(me->id) : std::vector{}; + check(!threads.empty() && std::ranges::any_of(threads, [&](const auto& t) { return t.id == me->id; }), + "the threads of this program are listed, its first among them"); + } + { + // 0.2.1: `NAME=value' entries as a started program's environment. + const std::vector entries{ "OS_TEST_VALUE=from-entries", "=C:", "NOEQUALS", "EMPTY=" }; + const auto pairs = os::env::assignments(entries); + check(pairs.size() == 2 && pairs[0].first == "OS_TEST_VALUE" && pairs[0].second == "from-entries" + && pairs[1].first == "EMPTY" && pairs[1].second == "", + "entries become overrides; one without a name before its `=' is left out"); + auto o = copy("env"); + o.env = pairs; + auto r = os::process::run(o); + check(r && r->out == "from-entries", "and reach the program"); + } { auto r = os::process::run(copy("relay")); check(r && r->status.success() && r->out.empty() && r->err == "said", From 211e765edbed99ef02526c45af9da988f6e48399 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 07:40:46 +0900 Subject: [PATCH 6/8] the ok-image targets take their C library, C++ run-time and loader from their platform (mcpp SPEC-013); CI locates the runner through the target's configuration Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 28 +++++++++++++++++++--------- CHANGELOG.md | 6 ++++++ README.md | 6 ++++-- mcpp.toml | 10 +++------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4466e3..5cb71f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,15 +291,25 @@ jobs: run: | xlings install "mcpp@$MCPP_VERSION" -y -g mcpp self config --mirror GLOBAL - mcpp toolchain install llvm 23.1.3 - mcpp toolchain default llvm@23.1.3 - # The okrun of the openkal-tools version this package's manifest names. - tools=$(sed -n 's/^openkal-tools = { version = "\([^"]*\)".*/\1/p' mcpp.toml) - [ -n "$tools" ] || { echo "::error::mcpp.toml names no openkal-tools version"; exit 1; } - git clone --quiet --depth 1 --branch "$tools" https://github.com/mcpplibs/openkal-image.git "$RUNNER_TEMP/oi" - ( cd "$RUNNER_TEMP/oi/tools" && mcpp build --release ) - okrun=$(find "$RUNNER_TEMP/oi/target" -path '*bin*' \( -name okrun -o -name okrun.exe \) -type f | head -1) - [ -n "$okrun" ] || { echo "::error::okrun did not build"; exit 1; } + 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 -o -name mcpp.exe \) -path '*/bin/*' | head -1) + [ -n "$built" ] || { echo "::error::mcpp did not build"; exit 1; } + PATH="$(cd "$(dirname "$built")" && pwd):$PATH" + echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" + fi + # The platform's runner, as this machine's configuration of the + # target names it: the platform's toolchain installed, its tools + # built (mcpp SPEC-013 §13). + t='${{ matrix.arch }}-openkal' + cd "$RUNNER_TEMP" # outside the sparse checkout: the configuration of no project + mcpp toolchain install --target "$t" + py=python3; command -v python3 > /dev/null 2>&1 && python3 -c '' 2>/dev/null || py=python + okrun=$(mcpp self env --target "$t" --format json | $py -c 'import json,sys; print(json.load(sys.stdin)["data"]["runner"]["program"] or "")') + [ -n "$okrun" ] || { echo "::error::the platform's runner was not located"; exit 1; } echo "OKRUN=$okrun" >> "$GITHUB_ENV" - name: The same bytes, every suite run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b2ad18..3429589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 0.2.1 +- **The ok-image targets take their C library, C++ run-time and loader from + their platform.** `[target.'cfg(os = "openkal")']` no longer declares + openkal-llvm-runtime or openkal-tools: mcpp 2026.10.27.1 selects + openkal-platform for `-openkal` (mcpp SPEC-013). CI finds the runner + through `mcpp toolchain install --target` and `mcpp self env --target`. + The package builds and its tests pass in every arrangement of the program's C library on Linux, macOS and Windows: the system's own (glibc, libSystem, the Microsoft C runtime), and a C library that sits on openkal (openkal-musl with diff --git a/README.md b/README.md index c98e320..05707a9 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,10 @@ name's spelling is the host's as well: `os::fs::absolute()` and `os::fs::names_a_place()` read `C:\x` and `bin\x` as Windows does when the image runs there, where the standard library inside an image reads names as POSIX does. The -image carries its C library and C++ run-time (`openkal-llvm-runtime`); the -tests are built as images in CI and run through okrun on four systems, and +image carries its C library and C++ run-time, which with the loader are the +image's platform's (openkal-platform, selected by mcpp 2026.10.27.1 and later +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. ## License diff --git a/mcpp.toml b/mcpp.toml index 71d7f3c..0dd8445 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -90,10 +90,6 @@ sources = ["src/windows/posix/stdio.cpp"] [target.'cfg(os = "openkal")'.build] sources = ["src/targets/openkal.cppm", "src/openkal/*.cpp"] -# The tests of this package, built as images, carry the C library and the C++ -# run-time an image holds, and the loader that runs them. -[target.'cfg(os = "openkal")'.dev-dependencies] -openkal-llvm-runtime = "0.16.3" - -[target.'cfg(os = "openkal")'.build-dependencies] -openkal-tools = { version = "0.1.1", tools = ["okld", "okrun"] } +# The C library, the C++ run-time and the loader of an image are its +# platform's: mcpp 2026.10.27.1 and later select openkal-platform for +# `-openkal` (mcpp SPEC-013), so this package declares none of them. From 988ce2b8347711500564001a4bc6134e3fd10a27 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 12:17:24 +0900 Subject: [PATCH 7/8] checks/hosted: openkal-platform 0.17.3's hosted arrangement states the toolchain, the C library and the C++ run-time (no layer named here) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- checks/hosted/mcpp.toml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/checks/hosted/mcpp.toml b/checks/hosted/mcpp.toml index 4c0a819..b40302b 100644 --- a/checks/hosted/mcpp.toml +++ b/checks/hosted/mcpp.toml @@ -1,20 +1,19 @@ # The package's tests in the arrangement a program over openkal chooses on a -# system's own triple: the C library is openkal-musl and the C++ run-time is -# openkal-llvm-runtime, both above openkal's implementation for that system -# (mcpp docs/24; mcpp design 2026-10-28 §5.4). The units of this package that -# reach the system beside openkal then see musl's headers, POSIX's names and -# the data model and `wchar_t' the C library declares, not the system's SDK. +# system's own triple: openkal-platform's hosted arrangement (mcpp SPEC-013 +# §5.4), whose toolchain, C library (openkal-musl) and C++ run-time +# (openkal-llvm-runtime) stand above openkal's implementation for that system. +# The units of this package that reach the system beside openkal then see +# musl's headers, POSIX's names and the data model and `wchar_t' the C library +# declares, not the system's SDK. [package] name = "os-check-hosted" version = "0.1.0" -[toolchain] -default = "llvm@23.1.3" - [dependencies] -os = { path = "../.." } -openkal-musl = { version = "0.22.3", default-features = false, features = ["posix"] } -openkal-llvm-runtime = "0.16.3" +os = { path = "../.." } + +[target.'cfg(any(linux, macos, windows))'] +platform = "openkal-platform@0.17.3" # The tests are the package's own, each reached by an include from # tests/.cpp here (a test program is a file of the package that runs it). From 3ca21e65d6b15f012be478e61e87babd852ce580 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Wed, 28 Oct 2026 18:28:23 +0900 Subject: [PATCH 8/8] ci: mcpp 2026.10.27.1, the release that reads the platforms the ok images and checks/hosted stand on Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cb71f2..6b718a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,11 @@ name: CI # platform a platform module used outside `if constexpr' for its platform # fails to LINK, and the same use in a discarded branch links (R5) # -# 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 -# module through a third under GCC (`mcpplibs.os.terminal' through -# `mcpplibs.os.error', mcpp part 2 design §10.2) and builds the ok images. +# The library is built with the released mcpp, 2026.10.27.1 or later: the +# first that selects a platform (SPEC-013), which the ok images and +# checks/hosted take their C library and C++ run-time from (openkal-platform +# 0.17.3), after 2026.10.26.2 compiled a unit with a module map importing +# another package's module through a third under GCC. # A dispatch may name a branch of mcpp instead (`mcpp_ref'), which is then # built and used. @@ -30,7 +31,7 @@ on: env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF || '' }} - MCPP_VERSION: 2026.10.26.2 + MCPP_VERSION: 2026.10.27.1 XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1'