diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc21c11..ef57f13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ on: default: "" env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} - MCPP_VERSION: 2026.8.26.2 + MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' @@ -82,6 +82,34 @@ jobs: else xlings install "mcpp@$MCPP_VERSION" -y -g fi + # ⚠️⚠️ TRANSITION: GIVE THE BOOTSTRAP THE glibc ITS BINDING NAMES. + # + # `xim:glibc`'s `latest` moved from `2.44` to `2.44.2`. A payload + # directory is named after the version a request RESOLVED to, while a + # RuntimeBinding carries the version that was DECLARED — and the xlings + # a released mcpp vendors into its own sandbox still declares `2.44`. + # So a clean machine installs `2.44.2`, the toolchain fixup asks for + # `2.44`, and the build stops before anything is compiled: + # + # error: selected RuntimeBinding glibc@2.44 requires payload + # '…/xpkgs/xim-x-glibc/2.44', but it is not installed + # + # ⚠️ On every NEW machine and on none that already existed, which is why + # it is invisible from a developer's own. Measured on `main` as readily + # as on any branch — the index records the same failure verbatim in + # `pkgs/g/glibc.lua` and states the rule it broke: "The index is DATA + # and the client is a PROGRAM: the consumer ships first." + # + # ⭐ REMOVE THIS once a released mcpp resolves it. `mcpp 2026.8.27.1` + # accepts an installed payload whose version REFINES the requested one + # (`payload_dir_for_version`), so a bootstrap from it needs nothing + # here. Until then the missing payload is simply installed. + if [ -x "$HOME/.mcpp/registry/bin/xlings" ]; then + XLINGS_HOME="$HOME/.mcpp/registry" XLINGS_NON_INTERACTIVE=1 \ + "$HOME/.mcpp/registry/bin/xlings" install glibc@2.44 -y -g \ + >/dev/null 2>&1 || true + echo "glibc payloads present: $(ls "$HOME/.mcpp/registry/data/xpkgs/xim-x-glibc" 2>/dev/null | tr '\n' ' ')" + fi mcpp --version mcpp self config --mirror GLOBAL # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. @@ -135,6 +163,55 @@ jobs: echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi + # EVERY NAME THE HEADER DECLARES IS EXPORTED BY ONE OF THE .def FILES. + # + # `port/*.def` is an explicit list, and an import library generated from it + # contains exactly those names. So a declaration added to `src/win32.h` + # without a matching line in a `.def` compiles, and fails at the link of a + # CONSUMER --- not of this package, which does not link. + # + # ⚠️ MEASURED. Three declarations were added for openkal 0.8 and the names + # were not, and this package's own CI stayed green: the failure appeared in + # openkal-llvm-runtime's cross-build, one repository away, as + # + # ld.lld: error: undefined symbol: __declspec(dllimport) CreatePipe + # + # which reads as a defect in the consumer. The check belongs here, where + # the two lists are. + - name: Every declared name is exported by a .def + run: | + python3 - <<'PY' + import glob, os, re, sys + # ⚠️ THE ENCODING IS NAMED. Python opens a file with the platform's + # default, which on this runner is cp1252, and these sources are UTF-8: + # + # UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 + # + # A check that reads source files must say what they are encoded in, or + # it reports on the runner's locale. + declared = set(re.findall(r'OKW_IMPORT\s+\w+\s+OKW_API\s+(\w+)\s*\(', + open("src/win32.h", encoding="utf-8").read())) + exported = set() + for f in glob.glob("port/*.def"): + body = open(f, encoding="utf-8").read().split("EXPORTS", 1) + if len(body) < 2: continue + exported |= {l.strip() for l in body[1].split("\n") + if l.strip() and not l.lstrip().startswith(';')} + # A denominator on both sides: with either list empty the difference is + # vacuously empty too. + if not declared or not exported: + print(f"::error::declared={len(declared)} exported={len(exported)}; nothing was compared") + sys.exit(1) + missing = sorted(declared - exported) + print(f" {len(declared)} declared, {len(exported)} exported across " + f"{len(glob.glob('port/*.def'))} .def files") + if missing: + print("::error::declared in src/win32.h and exported by no .def:") + for m in missing: print(f" {m}") + sys.exit(1) + print(" ok every declared name is exported") + PY + - name: Select the toolchain run: | spec='${{ matrix.toolchain }}' diff --git a/mcpp.toml b/mcpp.toml index 2a6812b..ffc8aa0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.1.5" +version = "0.2.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-windows" [dependencies] -openkal = "0.7.0" +openkal = "0.8.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/port/kernel32.def b/port/kernel32.def index 92175b1..2a5e1ee 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -6,12 +6,14 @@ LIBRARY KERNEL32.dll EXPORTS CloseHandle CreateFileW +CreatePipe CreateProcessW CreateThread FlushFileBuffers FreeEnvironmentStringsW GetCommandLineW GetConsoleMode +GetConsoleScreenBufferInfo GetCurrentDirectoryW GetCurrentProcess GetCurrentThreadId @@ -31,6 +33,7 @@ MultiByteToWideChar QueryPerformanceCounter QueryPerformanceFrequency ReadFile +SetConsoleMode SetFilePointerEx SetHandleInformation Sleep diff --git a/src/process.cpp b/src/process.cpp index 7049e72..8af7600 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -177,6 +177,81 @@ int kal_process_spawn(kal_dir base, return kal_ok; } +// A channel: a pair of streams of which one end is meant to cross a spawn. +// +// THIS ENVIRONMENT DECIDES INHERITANCE PER HANDLE AND NOT PER EXEC, which is the +// opposite of the other two and is why the far end is created inheritable while +// the near end is not. On a descriptor system every handle is inherited unless +// marked otherwise, so those implementations mark the ends close-on-exec and let +// the spawn place the far one deliberately. Here the default is not to inherit, +// so the far end must be marked to be inheritable and the near end must be left +// alone --- otherwise the started program would hold both ends and the writer +// would never observe the end of input. +int kal_process_channel(kal_stream* mine, kal_stream* theirs) { + if (mine == nullptr || theirs == nullptr) return kal_err_invalid; + + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof sa; + sa.bInheritHandle = TRUE; + + HANDLE reading = nullptr, writing = nullptr; + if (!CreatePipe(&reading, &writing, &sa, 0)) + return okw::translate_win32(GetLastError()); + + // The near end is withdrawn from inheritance after the fact, because + // CreatePipe applies one set of attributes to both. + SetHandleInformation(reading, HANDLE_FLAG_INHERIT, 0); + + // Bare handles rather than packed ones, because openkal.stream's transfer + // operations take what this environment takes. kal_fs_stream reports a + // file's stream the same way and for the same reason. + *mine = kal_stream{ reinterpret_cast(reading) }; + *theirs = kal_stream{ reinterpret_cast(writing) }; + return kal_ok; +} + +void kal_process_channel_close(kal_stream s) { + void* h = reinterpret_cast(s.h); + if (h == nullptr || h == INVALID_HANDLE_VALUE) return; + // The standard streams are borrowed. Closing one through this operation + // would take a stream away from the whole program. + if (h == GetStdHandle(STD_INPUT_HANDLE) || + h == GetStdHandle(STD_OUTPUT_HANDLE) || + h == GetStdHandle(STD_ERROR_HANDLE)) return; + CloseHandle(h); +} + +// Starting a program that receives exactly the directories named. +// +// ⚠️ NOT PROVIDED, AND THE REFUSAL IS THE HONEST ANSWER RATHER THAN A GAP. A +// preopened directory is a handle a started program reads back through +// kal_fs_preopen by NUMBER, and this environment has no numbering: a handle +// crosses a spawn by being inheritable, and the started program learns of it +// through a mechanism the parent has to arrange itself. There is no +// correspondence here to descriptor three. +// +// Clause 6.2 is what makes the refusal conforming rather than a deviation: the +// operation exists, reports kal_err_not_supported, and the property word does +// not claim KAL_PROCESS_PROP_GRANT_DIR. A caller therefore learns from the word +// what it would otherwise learn from a failed call. +int kal_process_spawn_with(kal_dir base, + const char* path, kal_uintptr path_len, + const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, + const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, + const kal_spawn_streams* streams, + const kal_preopen* grants, kal_uintptr grant_count, + kal_process* out) { + // A count of zero asks for a program with no preopens, which this + // environment gives a started program anyway --- it has none to pass. That + // request is therefore answerable, and is answered by the ordinary spawn. + if (grant_count == 0) + return kal_process_spawn(base, path, path_len, + argv, argv_lens, argc, + envp, envp_lens, envc, streams, out); + (void)grants; + return kal_err_not_supported; +} + int kal_process_wait(kal_process p, int* status, int* terminated) { void* h = okw::unpack(p.h); if (!h) return kal_err_invalid; @@ -212,8 +287,12 @@ void kal_process_close(kal_process p) { if (h) { okw::retire(p.h); CloseHandle(h); } } +// KAL_PROCESS_PROP_GRANT_DIR is deliberately absent: kal_process_spawn_with +// refuses a non-empty set of grants here, and a word claiming a facility the +// next call refuses is the disagreement clause 6.2 exists to prevent. const kal_uintptr kal_process_props = KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING - | KAL_PROCESS_PROP_EXIT_STATUS; + | KAL_PROCESS_PROP_EXIT_STATUS + | KAL_PROCESS_PROP_CHANNEL; } diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..2fb9e5c --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,107 @@ +#include "win32.h" +#include + +// openkal.terminal upon this environment's console operations. +// +// THE TWO POSITIONS THIS INTERFACE DEFINES ARE TWO CONSOLE FLAGS, AND THEY ARE +// NOT THE SAME TWO A UNIX TERMINAL HAS. This environment assembles lines under +// ENABLE_LINE_INPUT and shows what is typed under ENABLE_ECHO_INPUT, which is +// the same division openkal.terminal draws; what differs is that the flags live +// on the INPUT handle and this interface takes a stream. A caller asking about +// the output handle is answered for the output handle, which reports neither +// position, and that is correct rather than a limitation: the mode of an output +// console is not what the interface asks about. + +namespace { + +void* handle_of(kal_stream s) { return reinterpret_cast(s.h); } +bool valid(void* h) { return h != nullptr && h != INVALID_HANDLE_VALUE; } + +// This environment's console input flags. +constexpr DWORD enable_line_input = 0x0002; +constexpr DWORD enable_echo_input = 0x0004; + +} // namespace + +extern "C" { + +int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { + if (mode == nullptr) return kal_err_invalid; + void* h = handle_of(s); + if (!valid(h)) return kal_err_invalid; + + DWORD m = 0; + // Reading a console's mode succeeds for a console and fails otherwise, which + // is the same enquiry kal_stream_props performs. A stream that is not a + // console is therefore reported as unsupported rather than as an error of + // this environment. + if (!GetConsoleMode(h, &m)) return kal_err_not_supported; + + kal_uintptr out = 0; + if ((m & enable_line_input) != 0) out |= KAL_TERM_LINE_EDIT; + if ((m & enable_echo_input) != 0) out |= KAL_TERM_ECHO; + *mode = out; + return kal_ok; +} + +int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { + void* h = handle_of(s); + if (!valid(h)) return kal_err_invalid; + + // READ, MODIFY, WRITE. The console mode carries processed input, mouse + // input, window input and virtual terminal processing, none of which this + // interface names. Writing a word composed from the two positions alone + // would turn all of them off, and a program that asked only to stop echoing + // would find its console changed in ways it did not ask for. + DWORD m = 0; + if (!GetConsoleMode(h, &m)) return kal_err_not_supported; + + if ((mode & KAL_TERM_LINE_EDIT) != 0) m |= enable_line_input; + else m &= ~enable_line_input; + if ((mode & KAL_TERM_ECHO) != 0) m |= enable_echo_input; + else m &= ~enable_echo_input; + + // A position this implementation does not distinguish is ignored rather than + // refused, which clause 6.2 requires of a word. + if (!SetConsoleMode(h, m)) return kal_err_not_supported; + return kal_ok; +} + +int kal_terminal_size(kal_stream s, kal_uintptr* cols, kal_uintptr* rows) { + if (cols == nullptr || rows == nullptr) return kal_err_invalid; + void* h = handle_of(s); + if (!valid(h)) return kal_err_invalid; + + CONSOLE_SCREEN_BUFFER_INFO_ info{}; + // Both outputs are left untouched on failure, which the interface requires. + if (!GetConsoleScreenBufferInfo(h, &info)) return kal_err_not_supported; + + // THE WINDOW AND NOT THE BUFFER. This environment's screen buffer may be + // taller than the window that shows it, and a program drawing a full screen + // against the buffer's height would scroll its own output away. The window + // is what a caller asking for the size of the display means. + *cols = static_cast(info.srWindow.Right - info.srWindow.Left + 1); + *rows = static_cast(info.srWindow.Bottom - info.srWindow.Top + 1); + return kal_ok; +} + +kal_uintptr kal_terminal_props(kal_stream s) { + void* h = handle_of(s); + if (!valid(h)) return 0; + + kal_uintptr p = 0; + + DWORD m = 0; + if (GetConsoleMode(h, &m)) p |= KAL_TERM_PROP_MODE; + + // Asked for rather than derived from the first: an input console answers the + // mode and not the size, and an output console answers the size and not the + // mode. Deriving either from the other would make the word claim a facility + // the next call refuses. + CONSOLE_SCREEN_BUFFER_INFO_ info{}; + if (GetConsoleScreenBufferInfo(h, &info)) p |= KAL_TERM_PROP_SIZE; + + return p; +} + +} // extern "C" diff --git a/src/win32.h b/src/win32.h index cf71357..7e0199f 100644 --- a/src/win32.h +++ b/src/win32.h @@ -244,7 +244,26 @@ OKW_IMPORT BOOL OKW_API CloseHandle(HANDLE); OKW_IMPORT DWORD OKW_API GetLastError(void); OKW_IMPORT DWORD OKW_API GetFileType(HANDLE); OKW_IMPORT BOOL OKW_API SetHandleInformation(HANDLE, DWORD, DWORD); +// For kal_process_channel. The security attributes decide whether the ends are +// inheritable, which is what makes one of them able to cross a spawn. +OKW_IMPORT BOOL OKW_API CreatePipe(HANDLE*, HANDLE*, SECURITY_ATTRIBUTES*, DWORD); OKW_IMPORT BOOL OKW_API GetConsoleMode(HANDLE, DWORD*); +OKW_IMPORT BOOL OKW_API SetConsoleMode(HANDLE, DWORD); + +// The console's dimensions, for openkal.terminal. The structure is this +// environment's and is declared here for the reason every other structure in +// this file is: it belongs to the environment rather than to a C library, and +// this implementation has none to take it from. +struct COORD_ { short X; short Y; }; +struct SMALL_RECT_ { short Left; short Top; short Right; short Bottom; }; +struct CONSOLE_SCREEN_BUFFER_INFO_ { + COORD_ dwSize; + COORD_ dwCursorPosition; + unsigned short wAttributes; + SMALL_RECT_ srWindow; + COORD_ dwMaximumWindowSize; +}; +OKW_IMPORT BOOL OKW_API GetConsoleScreenBufferInfo(HANDLE, CONSOLE_SCREEN_BUFFER_INFO_*); OKW_IMPORT BOOL OKW_API ReadFile(HANDLE, LPVOID, DWORD, DWORD*, OVERLAPPED*); OKW_IMPORT BOOL OKW_API WriteFile(HANDLE, LPCVOID, DWORD, DWORD*, OVERLAPPED*);