An implementation of openkal for Windows.
[dependencies]
openkal = "0.23.0"
[target.'cfg(windows)'.dependencies]
openkal-windows = "0.19.0"Its purpose is as much to test the specification as to be used. openkal was drawn against systems that share an ancestry; a third implementation on a system that shares none of it is what turns "this interface is not Unix's shape" from a claim into an observation.
The file interface is not a Unix shape. openkal has no global namespace of paths: every operation is relative to a directory the program holds. That was chosen because a global namespace is unavailable in a capability-based kernel, and the obvious objection is that no ordinary system works that way either.
This one does. Win32 has no operation that opens a name relative to a directory
handle — and the object manager Win32 is built upon does: NtCreateFile takes a
root directory in its object attributes and a name relative to it, and refuses a
name that leaves it. This implementation is written one layer below the
interface most programs on this system use, and there is nothing to resolve.
An implementation written on Win32 alone would have to recover each directory's
path with GetFinalPathNameByHandleW and concatenate, which is a name resolver
inside an implementation and is what clause 7.1 excludes. The interface asked
for the operation this system already had.
A declined interface is a legitimate outcome, and this system is where that
stops being theory. openkal.space starts a context in a copy of the calling
address space. There is no primitive here that does it: CreateProcessW starts
a named program, which is openkal.process and is a different operation.
Clause 3 says an implementation provides an interface in whole or not at all,
and this implementation provides fourteen of the fifteen — so a program that
calls kal_space_start fails at the link, naming the operation, which is
clause 6.1's report and is the loudest one available.
Constructing a copy out of CreateProcessW plus a mechanism for carrying the
caller's memory across would be the simulation clause 3.1 forbids: it would be
present, it would look like the operation, and what it produced would not be a
copy of the caller.
Duplication of the calling image is not a Unix preference. openkal.process
starts a program and does not duplicate one, and the specification's reason is
that duplication cannot be performed faithfully everywhere. This system has no
such operation at all, so an interface that offered one would have obliged this
implementation to construct it out of nothing. CreateProcessW is the composite
kal_process_spawn already is.
The suspension primitive is the shape of the thing. kal_task_wait compares
a word with an expected value and suspends without an intervening opportunity
for the value to change unobserved. It was drawn from one kernel's futex. This
system offers WaitOnAddress, which is the same operation, under a different
name, with no shared ancestry. A mutex built above it in the conformance suite
loses none of eighty thousand increments.
Names are compared without regard to case. kal_fs_props therefore does not
report prop_case_sensitive. A program that creates two names differing only in
case succeeds on the Linux implementation and not on this one, and no operation
could have told it which system it was on. This is what clause 6.2's properties
are for.
The monotonic source continues while the machine is suspended. The Linux
implementation's stops. prop_monotonic_suspends reports which, and a program
measuring an interval across a suspension needs to know.
A status does not say whether the program chose it. This system reports one number and does not distinguish a program that returned it from a program that was stopped. The implementation supplies a value for termination that an ordinary program does not return, so the two remain distinguishable — which is what the interface requires and all that it requires.
Handles are not small integers. Clause 6.6 requires that a released handle
not be treated as valid, and recommends dividing the word into an index and a
generation. The construction here is the same one the Linux implementation uses,
with the environment's handle recovered arithmetically from the word: it is not
a translation table, because no lookup decides what a word refers to. The bound
is stated in src/handle.h, and a handle beyond it is carried without a
generation rather than refused.
An implementation is selected by the program, and the program may itself supply the facilities the implementation would otherwise borrow. If it does, and the names agree, the implementation's calls resolve to the program's and the program's resolve back to the implementation.
This implementation therefore uses this environment's own interfaces and no C runtime symbol at all. CI asserts it against the objects: the only undefined names are Win32 imports, the object manager's, the suspension primitive's, and this implementation's own.
Whether this implementation is the whole of the program's environment. Ordinarily a program already carries a runtime that has received control from the loader, and this implementation adds nothing. Where it does not, the entry point is this implementation's and the link line names it — which is one function here, because this environment's loader has already established the argument vector, the named values and thread-local storage before it transfers control.
Fourteen of the fifteen. openkal.space is declined, for the reason above.
The four that openkal 0.8 added and this implementation now provides:
| on this system | |
|---|---|
openkal.net |
Winsock, started once at the first socket and never stopped. Every socket is made with WSA_FLAG_OVERLAPPED, and version 0.13 requires it: a synchronous socket shares one completion event between its two directions, so a read waiting in one thread held back a write from another on the same connection. The transfer operations issue ReadFile/WriteFile with an OVERLAPPED of their own and wait for it, so the two directions no longer contend for one event. A connection's stream placed in kal_spawn_streams reaches the started program as an overlapped handle: a program on this implementation transfers upon it correctly, and a program that issues synchronous ReadFile/WriteFile upon an inherited handle without an OVERLAPPED is outside what the system defines for a socket. A caller that starts such a program relays the connection through a channel (kal_process_channel) instead |
openkal.datagram |
the same calls with SOCK_DGRAM, WSASendTo/WSARecvFrom with their own OVERLAPPED for the same reason. This system reports a truncated message as a failure where the other two truncate silently; the bytes that fit are delivered either way, and the interface says the excess is lost |
openkal.timeout |
WSAPoll, which answers for sockets and for nothing else. A bounded read of a stream that is not a socket reports kal_err_not_supported — which the interface's own header anticipates in terms. kal_timeout_wait_process is the one operation of the interface this system provides directly, because a bounded wait upon an object is the primitive here |
openkal.exec |
VirtualAlloc writable, VirtualProtect executable, FlushInstructionCache. The third call is not optional and the other two systems' implementations do not need to make it explicit |
kal_task_stack reports the stack of the calling context, and this
implementation answers it with one call of this environment's own:
GetCurrentThreadStackLimits. Every context here is a thread this system built
--- the first one included --- so there is no record to keep and nothing to
measure, and the call cannot fail.
The call is Windows 8 and later, which is below every target this ecosystem names. The alternative reads the thread environment block with inline assembly or an intrinsic under three toolchains, and would be the first of either in this package.
Which bound it answers is not published: Wine implements it as the reservation
(DeallocationStack and StackBase) and WebKit reads it as the OS-maintained
limits. That is what it is taken to mean here, and what the conformance suite
observes is the part a caller depends on rather than the choice between the
two: the region contains the stack of the thread that asked. It is measured on
this system's own runner and again under Wine on a Linux one.
A region at an address. kal_exec_alloc_at reserves at the address, and
nothing where anything is there or the address is not a multiple of the
sixty-four kibibytes of reservation. KAL_EXEC_PROP_PLACE is claimed.
A part of a region is a file's (KAL_EXEC_PROP_FILE, where the system has
placeholders: Windows 10 1803). A region is a placeholder replaced by a view of a
section of the paging file of the region's size. A part made a file's is the
view unmapped back to a placeholder, split, the part replaced by a view of the
file and the rest by views of the same section at the same offsets, which hold
what they held; the region is a short table of those views. Views are placed in
sixty-four kibibytes, so a part not at that granularity is kal_err_not_supported
and the region is unchanged. KAL_EXEC_MAP_PREFETCH is PrefetchVirtualMemory.
kal_exec_seal_part makes a part read-only; KAL_EXEC_PROP_SEAL_FOREVER is not
claimed, because the holder of a view can always change its protection.
Grants. KAL_PROCESS_PROP_GRANT_DIR is claimed. A grant is a duplicate of the
caller's handle, inheritable for the one start and named in its explicit list;
an inherited handle keeps its value, and the started program learns the values,
the names and what each carries from KAL_PREOPENS and KAL_GRANT_RIGHTS, read
only where the number in them is the program that started it. A grant is made
uninheritable as it is taken over. KAL_SPAWN_CONFINE is refused: this system
holds a program to directories only through the files' security descriptors,
which a start does not change.
Attenuation and records (kal_fs_dir_attenuate): the table of the other two
implementations, indexed by a handle's slot. A listing answers that a name is
absent only where no other spelling could reach an entry: a name of ASCII with
no ~ (a short name the listing does not report), in a directory whose entries
are ASCII. A record is appended through a handle of its own opened with
FILE_APPEND_DATA, and ended by kal_exit or the runtime's terminators.
One request where the system has one. A listing reads
FileIdFullDirectoryInformation, whose records carry each entry's size, times,
attributes and identity (kal_fs_list_next_info; KAL_FS_PROP_LIST_INFO where
the volume states FILE_SUPPORTS_OPEN_BY_FILE_ID). kal_fs_info asks
NtQueryInformationByName for the node's record --- with the volume's serial
number from 24H2 (FileStatBasicInformation), and otherwise
(FileStatInformation, 1709) with the base directory's for a name of one
component --- and opens the node where the record cannot answer: a link to be
resolved, an identity across components, a volume that refuses the record.
Observation. A fault no handler takes is reported, by the filter this system
calls before it ends the program, with each frame's file, offset and symbol: the
file a part of a region maps (an ELF image) or the module (a PE image, from its
COFF symbol table). Samples are taken by a context of the program's own that
suspends each other context that used the processor since the last tick.
Touches are a part's execution taken away and given back, page by page, by a
first look at the access violations of the kind that executes.
kal_process_info reports page faults as one count, faults_minor.
kal_process_wait reports a program this system ended for a fault as
terminated. A preference for economy lowers the started program's memory
priority; kal_memory_info is GlobalMemoryStatusEx within the job's limits.
The null device at a position. KAL_SPAWN_NULL_IN, KAL_SPAWN_NULL_OUT and
KAL_SPAWN_NULL_ERR place NUL at the started program's standard input, output
or error; KAL_PROCESS_PROP_NULL_STREAMS is claimed. The device is opened for the
start, placed and made inheritable as a caller's stream is, and released after
it, so the caller holds no handle for it. A flag beside a stream at the same
position is kal_err_invalid. Measured on this system's runner and under Wine.
Instructions from a file are not provided. kal_exec_map_part reports
kal_err_not_supported and KAL_EXEC_PROP_FILE is not claimed: this system maps
a view of a file only where nothing is mapped, and a part of a reservation
becomes a view (MapViewOfFile3 with MEM_REPLACE_PLACEHOLDER) only where the
reservation was made of placeholders and the part split from it before anything
was committed there. A region would then be a set of pieces released one by
one, and this implementation would keep the table of them (clause 7.1). A
loader copies the file's bytes. An ended program's processor time was already
reported: the process object keeps it while the handle is held.
Transfers at an offset go through a second handle. A transfer given an offset on a handle opened for sequential transfer moves that handle's position here, and the specification says the position is neither used nor moved. Keeping a position of this implementation's own would part it from the system's, which the programs the file is given to share. So the file is opened a second time, relative to the first handle and with exactly its access, when the first transfer at an offset needs it; every transfer at an offset goes through that handle, whose position nothing observes, and it is closed with the file.
Regions are this system's reservation calls, from which the interface's shape
comes: VirtualAlloc reserves and commits, VirtualFree decommits and releases
whole, MEM_RESET discards. Backing is charged apart from reservation, so
KAL_REGION_PROP_OVERCOMMIT is not claimed. An alignment coarser than 64 KiB is
reserved wide, released, and reserved again at the aligned address, repeated if
another context took it meanwhile.
What a program and a unit use is the commit charge, which is what a job
counts and limits: K32GetProcessMemoryInfo for a program (kept by the process
object after the program ends, while the handle is held) and the job's own
ActiveProcesses and JobMemory for a unit.
A limit on a unit is the job's (KAL_PROCESS_PROP_JOB_LIMIT_HARD):
JOB_OBJECT_LIMIT_JOB_MEMORY refuses an allocation that would take the job over
its limit. The specification states that the whole unit ends, and the job alone
only refuses, so each limited job reports to one completion port and a context
started with the first limit ends a job when it reports reaching its limit. A
program that ended because an allocation was refused, before the rest of its job
was ended, is told apart by the job's record of the violation; kal_process_wait
reports both with KAL_TERMINATED_LIMIT.
A unit's termination is reported as one. kal_process_job_terminate ends the
job with the number kal_process_terminate uses, so its programs are reported as
ended rather than as having returned 15, which they were until 0.16.
A name that exists and is not a form this environment can start.
CreateProcessW reports ERROR_BAD_EXE_FORMAT for an image whose header this
loader does not recognise and ERROR_EXE_MACHINE_TYPE_MISMATCH for one built for
a different processor; both arrive here as kal_err_not_program rather than
kal_err_io, which is what let a caller distinguish a name that is not there
from one that is and cannot be started.
Whether a node may be started is not recorded on this system's ordinary
volumes. kal_fs_set_executable_at validates its arguments as its siblings do
and reports kal_err_not_supported; KAL_FS_PROP_EXECUTABLE is not claimed, and
kal_fs_info/kal_fs_file_info never set KAL_INFO_EXECUTABLE. Whether a
name may be started here is decided by its form when it is started, not read
from a property a volume stores.
A spawn inherits only the handles it placed. CreateProcessW with
inheritance enabled hands the started program every inheritable handle of this
process, not only the ones a caller named — a defect distinct from the
specification, found by a detached child that kept a starter's standard output
open long after the starter had gone. The fix is STARTUPINFOEXW with a
PROC_THREAD_ATTRIBUTE_HANDLE_LIST naming exactly the deduplicated, non-null
standard handles this operation placed; a handle the caller made inheritable for
some other reason and did not place is no longer inherited.
The conformance suite in the specification package, built for this target. The
feature set is enumerated rather than optional, and the difference is the one
interface this implementation declines: a set demanding space would fail to
link naming kal_space_start rather than report an observation.
git clone https://github.com/mcpplibs/openkal .spec
bash .spec/tools/run-conformance.sh openkal-windows . \
full,exec,random,terminal,net,datagram,timeout --target x86_64-windows-gnux86_64-windows-gnu (GCC producing PE), and the MSVC ABI through llvm. The
implementation uses no GNU extension.
The object manager's declarations were written out in src/win.h from the
beginning, with the reason recorded there: <winternl.h> is complete in one
toolchain's sources and partial in another's. That argument was always the same
argument, and it had only been applied to the half where one toolchain disagrees
with another rather than to the half where one MACHINE disagrees with another.
Declarations. src/win32.h now declares the whole of what this
implementation calls — forty-seven functions and the types and constants they
need — so nothing here opens <windows.h>. The list was not read out of the
sources: the header was removed and the compiler was asked what was missing.
Each name carries __declspec(dllimport), which is not an optimisation — it is
what makes the objects say that these are this environment's interfaces reached
through its import table, and this package's own independence check reads exactly
that.
Where they live. port/*.def names the library each of those forty-five
symbols is exported from, and the build program turns them into import libraries.
An import library is a list of names rather than code, which is why generating
one is a complete substitute rather than an approximation. The DLL each name
belongs to was read out of the corresponding mingw import library rather than
assumed — WaitOnAddress and its two neighbours are not in kernel32.dll,
and putting them there produces an import table that links and then fails to
bind.
Supplied only where the system's own are absent. On this system they are
present, they are the vendor's, and they list every name rather than the
forty-five this implementation calls; -L is searched first, so supplying ours
there would shadow them and a consumer calling a forty-sixth would be told there
is no such name.
Measured 2026-08-22, on a clean continuous-integration runner, after every object had compiled:
lld: error: unable to find library -lkernel32
The compile had been free of a vendor SDK since win32.h; the link had not.
Apache-2.0.