Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 }}'
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions port/kernel32.def
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ LIBRARY KERNEL32.dll
EXPORTS
CloseHandle
CreateFileW
CreatePipe
CreateProcessW
CreateThread
FlushFileBuffers
FreeEnvironmentStringsW
GetCommandLineW
GetConsoleMode
GetConsoleScreenBufferInfo
GetCurrentDirectoryW
GetCurrentProcess
GetCurrentThreadId
Expand All @@ -31,6 +33,7 @@ MultiByteToWideChar
QueryPerformanceCounter
QueryPerformanceFrequency
ReadFile
SetConsoleMode
SetFilePointerEx
SetHandleInformation
Sleep
Expand Down
81 changes: 80 additions & 1 deletion src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<kal_uintptr>(reading) };
*theirs = kal_stream{ reinterpret_cast<kal_uintptr>(writing) };
return kal_ok;
}

void kal_process_channel_close(kal_stream s) {
void* h = reinterpret_cast<void*>(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;
Expand Down Expand Up @@ -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;

}
107 changes: 107 additions & 0 deletions src/terminal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "win32.h"
#include <openkal/terminal.h>

// 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<void*>(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<kal_uintptr>(info.srWindow.Right - info.srWindow.Left + 1);
*rows = static_cast<kal_uintptr>(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"
19 changes: 19 additions & 0 deletions src/win32.h
Original file line number Diff line number Diff line change
Expand Up @@ -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*);
Expand Down
Loading