openkal defines an interface between a program and the environment that executes it. The interface is stated as a C application binary interface. It is distributed as a set of C headers that declare it and a set of C++ modules that export those declarations, so that a consumer written in either language reaches the same entities. Clause 4.2 records why the second way of reaching them is not a convenience.
The specification has two audiences. An implementation supplies the functions declared here and is judged by whether it can do so without constructing a compatibility layer. A consumer — an application, a C library, or a C++ standard library port — depends on those declarations and is judged by whether it can do so without knowing which implementation is present.
openkal does not describe an operating system, and it does not describe a program. It describes the boundary between them, and it is written so that the boundary can be crossed in either direction: an implementation may be built upon a C library, beneath one, or without one.
openkal is three statements at once: an interface stated in C (clause 4), an application binary interface at the boundaries of clause 4.4, and, for an ok image, the binary interface at run time between the image and the loader that binds it (clause 12). In each, what crosses is what this specification states, and nothing else.
Nothing of the environment beneath crosses upward. A value, a layout, an error, a name or a behaviour a consumer observes is one this specification defines. An implementation translates what its environment answers into these terms --- an error into clause 5.2's values, a capability into clause 6.2's properties --- and where its environment answers something the specification has no term for, it reports the nearest outcome the specification states or declines the operation; it does not pass the environment's own answer through. An identifier an environment gives (a program's, a context's) crosses as an opaque value, and its meaning is the one stated here, not the environment's.
Implementations may differ beneath, and not above. One calls its kernel, another a C library; a third composes an operation from others, or emulates it, and they differ in cost. They do not differ in what a consumer can observe, beyond what this specification leaves open, and what it leaves open it names: a property an implementation claims or does not (clause 6.2), one outcome among those an operation states (as a lowered claim on the processors may be refused raising, clause 11 entry 41). A consumer written against the specification runs on every implementation that provides what it uses, which is what the ecosystem above openkal is built upon.
A consumer that reaches past openkal binds itself to what it reached. A program may call its environment directly --- a system library, a system call --- and this specification neither forbids nor observes it. Such a program is portable as far as the rest of it is, and what it reached for is its own to provide elsewhere. An ok image cannot reach past: it imports only the names this specification declares and those other images export (clause 12.2), which is what lets the same bytes run on every system a loader serves.
The words shall, shall not and may carry their usual normative force. A conforming implementation satisfies every shall that applies to the interfaces it provides. An implementation is not required to provide every interface.
An interface is the unit of versioning and of provision. An implementation provides an interface in whole or not at all.
| Interface | Resource | Tier | S | L | X |
|---|---|---|---|---|---|
openkal.abort |
termination | core | ✓ | ✓ | ✓ |
openkal.stream |
a byte stream | core | ✓ | ✓ | ✓ |
openkal.memory |
a region of the address space | core | ✓ | ✓ | ✓ |
openkal.env |
the parameters a program receives at inception | optional | ✓ | ✓ | ✓ |
openkal.time |
a time source | optional | ✓ | ✓ | ✓ |
openkal.random |
a source of unpredictable bytes | optional | ✓ | ✓ | ✓ |
openkal.fs |
a directory, and an open file | optional | ✓ | ✓ | ✓ |
openkal.process |
a program image that has been started | optional | ✓ | ✓ | ✓ |
openkal.task |
an execution context, and a suspension primitive | optional | ✓ | ✓ | ✓ |
openkal.exec |
a region of the address space a program may execute | optional | ✓ | ✓ | ✓ |
openkal.terminal |
an interactive stream's treatment of what is typed | optional | ✓ | ✓ | ✓ |
openkal.net |
a connection, and a listener for connections | optional | ✓ | ✓ | ✓ |
openkal.datagram |
a message with a boundary, sent without a connection | optional | ✓ | ✓ | ✓ |
openkal.space |
an address space, and a context executing in one | optional | ✓ | ✓ | ✓ |
openkal.timeout |
a bound upon operations that would otherwise wait | optional | ✓ | ✓ | ✓ |
openkal.map |
a part of a file's contents in the address space | optional | ✓ | ✓ | ✓ |
openkal.region |
a range of the address space, reserved and backed as it is used | optional | ✓ | ✓ | ✓ |
openkal.prof |
what a program states about its own work to a measurement | optional | ✓ | ✓ | ✓ |
openkal.debug |
the name of an address in a program's own code | optional | ✓ | ✓ | ✓ |
openkal.event |
readiness of a set of resources | reserved |
Version 0.15 specifies the core and optional interfaces, version 0.18 adds
openkal.map (clause 11, entry 40), and version 0.20 adds openkal.region,
openkal.prof and openkal.debug (entries 44 and 45); versions 0.21 to 0.23 add no interface. The reserved row is not specified, and its
name shall not be used for other purposes.
The S, L and X columns state which boundaries an interface's declarations can cross. They are defined in clause 4.4 and are a statement about the shape of the declarations, not a requirement upon implementations: an implementation cannot violate them, and only a declaration can.
The five interfaces added in version 0.8 are optional in the sense clause 3
defines, and their optionality is not a concession. An environment with no
network is not deficient for providing neither openkal.net nor
openkal.datagram; an environment with no memory management unit cannot
provide openkal.space and is not deficient either. Clause 6.1 expresses each
absence as the absence of a definition at the link, so a program that requires
one of them is refused when it is built rather than when it runs.
Core denotes an interface every implementation provides. Optional denotes every other, and how its absence reaches a consumer is one of two things:
| how an optional interface is resolved | mechanism |
|---|---|
| the implementation does not provide it at all | clause 6.1 — the definitions are absent, and a consumer that uses one fails to link |
| the implementation provides it only to an artifact produced in a particular way | a feature of the implementation's package, resolved at dependency resolution — clause 6.5 |
There were three tiers, and the middle one said that an implementation
hosting a C library provides openkal.env, openkal.time, openkal.fs,
openkal.process and openkal.task. It was false, and falsified within this
specification's own ecosystem: an implementation for a machine with firmware and
no operating system provides none of the last three, and a C library is hosted
above it. The tier stated a fact that was not one, and it stated it about the
consumer rather than about the interface.
The honest classification is the Resource column beside it. An interface exists where its resource exists — storage, a second image, a scheduler, a network, an address space that can be copied, entropy, an interactive stream, a bound upon a wait. Clause 6.1 makes the absence a fact a consumer learns; no tier is needed to predict it, and the prediction was wrong.
Informative, and carrying no requirement: an implementation of a system with
storage and a scheduler ordinarily provides openkal.env, openkal.time,
openkal.fs, openkal.process, openkal.task and openkal.timeout. This
sentence describes what is common; nothing follows from it, and an
implementation is not measured against it.
An interface belongs to the core set when every plausible implementation can supply it without simulating a facility the environment lacks. The distinction between supplying and simulating is drawn as follows.
An implementation that would leave its callers silently wrong is a simulation. An implementation that merely bounds what is available is a supply.
A bump allocator over a fixed region can be exhausted, and exhaustion is a
defined outcome on every platform; such an allocator therefore supplies
openkal.memory. A counter that does not advance in real time would make every
timed wait incorrect without reporting it; such a counter would simulate
openkal.time, and openkal.time is accordingly not in the core set.
It follows that the presence of a heap is not a property of the hardware. Any
environment with writable memory can supply one, and the specification places
openkal.memory in the core set on that basis.
An interface admitted to the core set is required of every implementation
thereafter, including implementations of environments no one has yet written
one for. The set is therefore closed at openkal.abort, openkal.stream and
openkal.memory, and a later version shall place a new interface outside it
even where every implementation that exists at the time would satisfy it.
The asymmetry is the reason. An interface wrongly placed in the core set excludes environments — a bare machine, a supervisor, firmware — from conformance altogether, and that exclusion is discovered by whoever tries to write the implementation rather than by the specification. An interface wrongly placed outside it costs a consumer one declared dependency. The first error is found late and by the wrong party; the second is found at dependency resolution and by the party that can act on it.
What this closes is the set of INTERFACES. kal_version and
kal_interfaces are exported by every conforming implementation and belong to
no interface: they provide no resource, and an implementation answers both with
constants. They are the specification's own self-description, required by clause
9, and requiring them adds nothing to the core set that a bare machine would
have to supply. A consumer that is linked never needs to call them; a consumer
bound at load or across a boundary has no linker to ask, and asking an interface
by calling into it is the act that must not happen first.
An interface is provided or not provided, and clause 6.1 makes that a fact a consumer learns from the linker. A consumer must nevertheless be able to state what it needs before it is built, and stating it one interface at a time does not scale: the sets a program can be written against are not arbitrary subsets, and an ecosystem that could only enumerate them could not say "this package needs an environment of such-and-such a kind".
The specification names no such set, and the one it did name has been
withdrawn. Version 0.8 named hosted — the core interfaces together with
openkal.env, openkal.time, openkal.fs, openkal.process and
openkal.task. The name described a class of environment, and a name that
describes a class of environment is falsified by an environment nobody had in
mind. This one was falsified inside its own ecosystem within a release: a C
library is hosted above an implementation that provides none of the last three.
The scaling argument is also weaker than it read. There are fifteen interfaces, and a consumer that needs five names five. That is five lines, not a problem of scale, and it is what the ecosystem already does — a consumer states its required set per target, in its own package, which is where a convention among consumers belongs.
core remains, and is not a set a consumer states: it is the requirement upon
implementations stated in clause 3.2. Nothing in this clause alters clause 6.1:
an interface a consumer uses and an implementation does not provide is an
undefined symbol.
Naming is not factored into a separate interface. An earlier draft replaced
openkal.fs and openkal.net with a single interface mapping names to
resources, on the grounds that a file, a network connection and a serial port
all yield a byte stream. That decomposition was withdrawn for three reasons.
- It required every implementation to parse an unbounded set of name schemes, including implementations that support none of them. That requirement is a compatibility layer, and clause 7.1 excludes it.
- It merged the operations of unrelated resources. Positioning applies to a file and not to a connection; half-closure applies to a connection and not to a file. The merged interface would have contained operations that some of its resources can never satisfy, which clause 6.4 identifies as a defect.
- The precedent cited in its support was misread. The WebAssembly System Interface, second preview, separates resource kinds into distinct interfaces and shares only the stream type between them.
The stream is therefore the shared currency of the specification and not its common entrance.
The specification package provides one header per interface, which is where the declarations are, and one module per interface, which exports them. An implementation provides neither.
| Interface | Module | Header |
|---|---|---|
| shared definitions | openkal.types |
openkal/types.h |
openkal.abort |
openkal.abort |
openkal/abort.h |
openkal.stream |
openkal.stream |
openkal/stream.h |
openkal.memory |
openkal.memory |
openkal/memory.h |
openkal.env |
openkal.env |
openkal/env.h |
openkal.time |
openkal.time |
openkal/time.h |
openkal.fs |
openkal.fs |
openkal/fs.h |
openkal.process |
openkal.process |
openkal/process.h |
openkal.task |
openkal.task |
openkal/task.h |
openkal.random |
openkal.random |
openkal/random.h |
openkal.exec |
openkal.exec |
openkal/exec.h |
openkal.map |
openkal.map |
openkal/map.h |
openkal.h includes every header, for a consumer that uses several.
A consumer imports the interface. An implementation imports the same interface, because it needs the declarations it is defining, and it exports nothing.
consumer ──imports──► openkal ◄──imports── implementation
│ │
└──────────── links against ───────────────┘
Definitions of the functions the interface declares, and nothing else. An implementation that declares a module has misunderstood its role: the interface is the specification's, and an implementation that exported one would place a name the consumer relies upon under the control of a party the specification does not govern.
The consequence is that an implementation cannot extend the interface. This is not a restriction that must be enforced; it follows from the arrangement. An implementation may of course publish additional facilities, and it does so in its own modules, which a consumer that uses them must import by name.
The contract is a C application binary interface. The specification package states it once, as a C header per interface, and provides a C++ module per interface that includes that header in its global module fragment and exports the names it declares.
The second way of reaching the declarations is not a convenience. A C++ consumer imports the module for the interface it uses; a consumer written in C cannot, because a C translation unit has no import. The canonical consumer written in C is a C library being ported onto openkal, which is the case clause 1 names first, so a specification reachable only by import would be unusable by the consumer it exists for. Version 0.4 was reachable only by import, and the omission was found by attempting that port.
The arrangement is deliberately not two statements that agree. A module that re-declared what the header declares would be a second declaration of the same contract, and two declarations drift; here there is one declaration, and the module exports it. A consumer that imports and a consumer that includes therefore obtain the same entities, and no procedure is required to keep them equal because they are not two things.
The headers include no header of their own, name no library, and obtain the width of a machine word from the compiler. The second way of reaching the declarations therefore adds nothing to what the specification package requires, which remains nothing.
The module is not a translation of the header. It carries what C++ can check and C cannot, and both are checks rather than facilities.
The frozen layouts are asserted. Clause 5.3 declares the layout of every
structure immutable. A declaration that something shall not change is not a
mechanism; static_assert is. A handle being one machine word is the
difference between a value returned in a register and one returned through a
hidden pointer, which is a change of calling convention that no declaration
would report and that a consumer built against the earlier layout would not
survive. kal_node_info is the one structure that is permitted to grow, and it
carries self_size for precisely that reason — the assertion on it fixes the
offsets of the fields that exist, not the size of the whole.
The capability words become types that cannot be mixed. Clause 6.2 gives
each interface a word and positions within it, and every such word is a
kal_uintptr. A program that tests a file-system position against the task
word therefore compiles, runs, and answers a question nobody asked; the
position numbers are small and several interfaces have assigned the same ones,
so the answer is frequently the plausible one. In the module each interface's
positions carry the interface in their type, the operations that compose them
are defined only within one interface, and the mistake becomes a diagnostic.
Nothing is stored beyond the word and every operation is constant-evaluated.
The same reasoning applies to the flags of kal_fs_open, which are an intent
rather than a property and are a distinct type from either.
Two declarations, compared by a procedure. Not adopted: it is the arrangement this clause exists to avoid, and a procedure that compares them is a thing that can be omitted, skipped or made to pass.
Generating the header from the modules. Not adopted: it makes the module form normative, which contradicts clause 1, and it introduces a generator, which is a build-time dependency the specification package is written to avoid having.
Leaving the C form to each implementation. Not adopted for the reason clause 4.1 gives: a name the consumer relies upon would be under the control of a party the specification does not govern, and a C library ported onto openkal would take its declarations from whichever implementation it happened to be built against.
A second package carrying the C form. Not adopted: one contract in two packages can be resolved at two versions, and the property a contract has is that there is one of it.
A consumer that depends upon the specification and upon no implementation compiles and fails to link, and the diagnostic names the undefined functions:
undefined reference to `kal_stream_write'
undefined reference to `kal_stdout'
The failure is later than a compilation failure would be and is legible. An arrangement that reported it during compilation was considered and is described in clause 6.3, together with the reason it was not adopted.
Compile-time portability is the floor of this specification and not its goal. Every interface, operation and structure is designed so that it can also be crossed at run time, and where one cannot the reason is recorded in the table of clause 3 rather than left to be discovered.
Three boundaries are distinguished, because "crossed at run time" gives opposite answers for two of them:
| boundary | the implementation is | what it additionally forbids | |
|---|---|---|---|
| S | static | chosen when the artifact is built, and linked into it | nothing |
| L | late-bound | a separate object resolved at load, in the caller's address space; the artifact does not change when the implementation does | a structure whose size the consumer fixed when it was built; a report whose value the consumer fixed when it was built |
| X | crossed | in another address space or at another privilege level; a call is a trap or a message | a returned pointer into the implementation; a result wider than one machine word; a call back into the consumer |
Returning a pointer into the implementation is admissible at L and inadmissible at X; so is a result of two machine words. An interface marked for one is not thereby marked for the other.
An interface is X-capable when all six of the following hold.
- It exports no object. Everything an implementation reports about itself is an operation (clause 6.2).
- No operation returns a pointer into the implementation's storage. A value is copied into a buffer the caller supplies.
- No result is wider than one machine word. An operation whose whole result is
a count returns that count or the negated error value; an operation that
produces a resource returns
intand writes the resource through a pointer. - Every structure that crosses carries its own size, or states why it cannot grow.
- An entry is an address at which a context begins, not a function that is called. No return address exists in the started context.
- Its absence is discoverable by an operation and not only by a linker
(
kal_interfaces, clause 3.2).
The first four are decidable from SURFACE.txt and the declarations, and clause
9 asserts them. A new interface is designed against this rule before it is
specified. The reserved openkal.event is the first to which that applies.
kal_uintptr is an unsigned integer type wide enough to hold a pointer. It is
obtained from the compiler rather than from a header, because a freestanding
target may provide no header that defines it.
The set of error values is closed. An implementation maps the error conditions of its environment onto this set; it does not extend the set.
Mapping is not simulation. A table that translates one error value into another preserves the property that the implementation is natural for its environment, whereas reproducing a foreign namespace of names, descriptors or paths does not.
Detail beyond these values is not available. A per-thread channel carrying the
environment's own error value was considered and rejected: such a channel is
global mutable state with the defects of errno, and control flow that consults
it is not portable.
Version 0.5 adds five values, and records why, because the set being closed makes an addition to it the kind of change a reader is entitled to see argued.
| Value | Why the set could not express it |
|---|---|
kal_err_not_found |
Clause 7.7 already required it. The value did not exist, so both implementations reported a missing name as kal_err_invalid, and a caller could not distinguish a name that is absent from a handle that is wrong. The defect was in the enumeration rather than in the implementations. |
kal_err_exists |
kal_fs_open with exclusive, and kal_fs_mkdir, fail because the name is already there. It is an expected outcome, and mapping it to kal_err_io would report a medium failure for one. |
kal_err_not_empty |
Removing a directory that is not empty. Same reasoning. |
kal_err_is_directory |
A file operation applied to a directory. |
kal_err_not_directory |
A directory operation applied to a file, and a component of a name that is not a directory. |
Version 0.13 adds one value.
| Value | Why the set could not express it |
|---|---|
kal_err_not_program |
A name that exists and may be started, and is not in a form the environment can start. Every environment has the condition natively (ENOEXEC, ERROR_BAD_EXE_FORMAT). Before this value an implementation reported it as kal_err_io, which states that a device failed when none did; a C library above could then not map the failure back to the condition its own callers act upon. Measured by a consumer: posix_spawn of such a file returned EIO above openkal and ENOEXEC on a host (issue 28). |
The criterion for adding a value. The set is closed against implementations, which do not extend it; it is not closed against this specification, which adds to it under clause 8. A value is added only when both of the following hold, and the record of each addition states both:
- a caller acts differently upon the condition than upon every existing value, so that folding it into an existing value leaves the caller wrong rather than merely less informed; and
- every environment the specification targets has the condition natively, so that the mapping is one arm of a table and not a classification an implementation must construct (clause 7.1).
A condition for which either is not established is recorded and not added. The failures of a network connection are such a case today: the three implementations report them differently, and no measurement yet establishes which distinctions a caller acts upon.
The addition is governed by clause 8, which admits new declarations. A value already assigned retains its meaning, so a program compiled against version 0.4 observes the same values for the same conditions as before.
There is one rule, and it holds for every interface.
An operation whose whole result is a count of bytes returns
kal_intptr: the count, or the negated error value when no byte was produced. A count of zero is a count and not an error; for a read it denotes end of input.An operation that produces a resource, or that produces nothing, returns
intfromkal_errorand writes what it produced through a pointer.
Version 0.8 returned a structure of a count and an error from the operations that transfer. It is withdrawn, and the measurement that withdrew it is that every consumer of it collapsed the pair by hand and by this rule — report what was moved, or the condition when nothing was. The pair was never the shape a caller wanted, and a result of two words cannot be carried by a boundary that returns one (clause 4.4).
The collapse is sound because the error set of clause 5.2 is closed: a negated error occupies a small known range and can never be mistaken for a count. An open-ended error space would not permit it.
The layout of every structure declared by this specification is frozen at version 0.3. The evolution rule of clause 8 admits new declarations and excludes changes to existing ones; a structure layout is not protected by that rule unless it is separately declared immutable, and it is so declared here.
One structure is excepted, and the exception is stated in clause 4.2:
kal_node_info carries self_size, and fields may be appended to it. An
appended field does not move a field that exists, and an implementation writes
no more of the structure than the caller's self_size states. Version 0.13
appends executable and a reserved field that keeps the size a multiple of
eight on every target.
Every type that crosses this interface is one this specification declares.
kal_uintptr, kal_u32, kal_u64, kal_i64 and the structures of clause 5.3
are the whole vocabulary, together with void, char and int, which have one
meaning everywhere.
A declaration does not name long, size_t, uint64_t, or any other type
whose definition belongs to a C library or to a data model. This is not a
stylistic preference. sizeof(long) is eight on one common target and four on
another; a signature naming it would be one signature with two meanings, and the
property clause 1 exists for — that a program's source is invariant under a
change of implementation — would hold only among implementations that shared a
data model.
Clause 5.1 gives the reason the types are derived from the compiler rather than
from a header. This clause states the consequence for everything built above:
a layer above openkal uses nothing from a concrete backend, including its
types. The question "how wide is long here" does not arise at this interface,
and it does not arise because it cannot be asked.
The one layer for which this does not hold is a C library being ported onto
openkal, and it does not hold there for a reason that is not an exception: such
a library reconstructs POSIX, and POSIX itself names long. Its answer is a
property of the target rather than of openkal, and it is configured per target
accordingly.
This clause adds no declaration and alters none, so clause 8's rule is not engaged and the version does not advance on its account. Every declaration of version 0.6 already satisfies it — one hundred and one of them, examined by the procedure below — and what is new is that the property is now stated and checked rather than held by the care of whoever wrote each header. The five interfaces added in version 0.8 were examined by the same procedure and satisfy it also; the count is now one hundred and forty-six.
Clause 9's procedure examines this.
The declarations are the same on a target whose machine word is thirty-two bits
wide, and they mean the same. kal_uintptr and kal_intptr are thirty-two bits
there; a position in a file, a size of a file, a duration and an instant remain
kal_i64 and kal_u64, so that a file larger than four gibibytes and a time
after 2038 are as reachable as elsewhere. Four consequences follow, and each is
a requirement upon an implementation for such a target rather than a change to a
declaration.
- A count is a word. An operation that reports a count reports it in
kal_intptr. A transfer whose length exceeds the largest such value is a partial transfer, reported as any partial transfer is (clause 7.4); it is not a wrapped count. - A layout is the target's. A structure of clause 5.3 is laid out as the
target's C ABI lays out its declaration; on i386 a
kal_u64field is aligned to four bytes and not eight. The immutability of clause 5.3 is that of the declaration, and both sides of the interface compile the same declaration with the same compiler, so they agree. An implementation does not copy a structure between a sixty-four bit and a thirty-two bit layout. - A handle fits the word. A handle is a
kal_uintptr. An implementation that packs more than an index into a handle --- a generation, a kind --- packs it into thirty-two bits on such a target, and a generation that wraps sooner is acceptable where clause 7.2's staleness is still detected for the reuse the environment can make within one wrap. - Time is sixty-four bits at the environment, too. An environment that offers both a thirty-two bit and a sixty-four bit form of a time operation is called through the sixty-four bit form, where the environment has one: a thirty-two bit second count ends in 2038, and an answer that wraps there is a wrong answer, not an approximation.
Images (clause 12) are of one architecture; an image of a thirty-two bit architecture runs on a loader of that architecture. Clause 11, entry 62.
An interface that an implementation does not provide is absent as a link-time definition, and a consumer that uses it fails to link. A conforming implementation shall not provide an interface whose operations report a lack of support at run time; the specification treats run-time refusal as a defect and not as a means of expressing partiality.
An interface that the specification does not define is absent as a module, and a consumer that imports it fails to compile.
An operation an implementation may lack becomes an interface of its own, so that its absence is reported by the linker. A property that varies between implementations is reported by a capability word.
The two are not alike, and the distinction is the one clause 6.4 already draws. An operation that is present and always fails is a defect; the remedy is that its absence be expressed by its absence. A property cannot be called: whether names are compared case-sensitively, whether a clock advances while the machine is suspended, what the granularity of a measurement is. A program adapts to a property rather than invoking it.
Each interface that has properties declares a word named kal_<interface>_props
and the positions within it. A position, once assigned, retains its meaning; a
position that has not been assigned reads as zero, so that a program compiled
against a later specification behaves correctly against an earlier
implementation.
A position is therefore assigned the sense in which zero is the weaker claim. An implementation that has never heard of a position answers with zero, and that answer is a statement about the implementation which has to be true; a position spelled the other way about would make the silence of every earlier implementation assert the stronger thing. The rule is stated here because it was recognised while assigning one: version 0.14 states whether an environment reserves keystrokes for itself, and the spelling in which zero meant reserves none would have had every implementation released before it claim a guarantee that none of them provides.
A property that varies between the resources of an interface rather than
between implementations cannot be a word, because there is no one answer to
record in it. Such a property is reported by an enquiry taking the resource.
kal_stream_props is the example: the same implementation answers differently
for a terminal and for a file. The enquiry is not the defect clause 6.4
describes, because it is not an operation upon the resource --- nothing is
transferred, and no resource can fail to answer.
Information therefore becomes available at three times, each being the earliest at which it exists.
| Time | Mechanism | Question answered |
|---|---|---|
| dependency resolution | the implementation package declares what it provides | may this program be built against this implementation |
| link | an undefined symbol | was an interface used that the implementation does not provide |
| run | a capability word | how does this implementation behave within an interface it provides |
The rule of clause 6.2 replaced three arrangements that were built before it was recognised. They are recorded because each is the arrangement a reader is likely to propose, and because the measurements that exclude them remain useful.
A module supplied by the implementation, declaring the optional operation, so that a consumer importing it without a supporting implementation fails to compile with the module named. Not adopted: it reintroduces, for the optional operation, the arrangement clause 4.1 rejects for the interface.
Fallback overloads in the interface, displaced by an implementation declaring its own, with presence detected through argument-dependent lookup. Not adopted: the arrangement requires an implementation's declarations to be visible to the consumer, which requires the implementation to own the module the consumer imports, which contradicts clause 4.
A record of capability flags, accompanied by a file in which an implementation declares them. Not adopted: a record can disagree with the code it describes, and the file is a second place in which a package is configured.
Two further arrangements were weighed while specifying version 0.8 and are recorded on the same basis.
Readiness notification. An interface reporting that a stream may be read, by
waking a word as kal_task_wake does, was considered as the remedy for a context
that would otherwise wait without end. It composes better than the bound this
specification adopted: one operation covers every waitable resource, a single
context may await many sources, and a library above it needs no read-ahead buffer
because a notification consumes nothing.
It was not adopted because of what it asks of an implementation. On an
environment whose readiness is discovered by polling a set of descriptors, an
implementation would have to maintain that set and a context of its own to watch
it. That is a mechanism reconstructed rather than a facility conveyed, which
clause 7.1 excludes. openkal.timeout asks the same environment only for what it
already does at the point of the call.
A space as a handle. An earlier form of openkal.space separated the copying
of an address space from the starting of a context in it, so that a caller held a
space and could start a context in it afterwards. It was withdrawn while the
first implementation was being written.
No environment this specification targets has that pair as a primitive. The copy and the start are one act, and an implementation asked to separate them would have to start a context anyway, park it upon a waiting primitive, and build a channel by which to tell it what to run. Clause 7.1 identifies that as a fault in the shape of the specification rather than in the implementation, and the separated form was the shape at fault. The single operation that replaced it is what every such environment already performs.
An instant rather than a duration. openkal.timeout states a duration
because kal_task_wait does. An instant would not accumulate drift when a caller
retries in a loop, and was considered for that reason. It was not adopted because
it would give one specification two spellings of one idea. A caller that requires
an instant computes the remaining duration from kal_time_monotonic, so the cost
falls upon the caller that has the requirement rather than upon every
implementation.
The measurements that constrain any future proposal:
- A requires-expression naming a qualified entity that does not exist is ill-formed. Detection through unqualified lookup and argument-dependent lookup is well-formed and evaluates to false.
- Argument-dependent lookup does not reach a module the translation unit has not imported.
- A module whose name extends another module's name by a further component is treated by some build systems as a submodule of it, and the two then form a dependency cycle.
An operation that some resources of an interface can never satisfy shall not be
placed in that interface. Positioning is the example the specification records.
On a hosted system, whether a stream can be repositioned is a property of the
individual stream rather than of the implementation: the same implementation
succeeds for a regular file and fails for a pipe. An implementation could
therefore neither claim the operation honestly nor withhold it usefully.
Positioning accordingly belongs to openkal.fs, whose resource is a descriptor.
An interface may be one the environment can provide only to a program that was
produced in a particular way. openkal.exec is the instance: one system grants
a program memory it may execute only when the program carries a signed
declaration that it will ask for such memory, and that declaration is applied
after the link, by whoever produces the artifact.
This is neither of the two partialities clause 6.2 distinguishes. The implementation does not lack the operation, so withholding the interface outright would deny it to programs for which it does work. Nor does it vary between the resources of the interface, which is clause 6.4's case; every region in a given program behaves alike.
Such an interface shall be provided or withheld at dependency resolution — the first of the three times clause 6.2 tabulates, and the earliest at which the question can be answered, because the party that decides how the artifact is produced is the party that resolves its dependencies. An implementation expresses it as a feature of its package. A program that asks for the feature gets the interface and an artifact produced so that it works; a program that does not ask gets neither, and using the interface fails to link, which is clause 6.1's ordinary report.
The rule this states is general: an operation whose availability is decided by how the artifact is produced is not reported at run time. Reporting it at run time would make every caller carry a path that most artifacts never take — and a path no artifact takes is a path nothing has verified.
The rule holds where the artifact is produced for its own environment, and
inverts where it is not. An artifact that is distributed is produced once, by
a party that decided for every environment it will meet, possibly long before
and for a different system; the consumer resolves nothing. So availability
settled this way is also reported at run time, by a position in the
interface's property word — KAL_EXEC_PROP_AVAILABLE. The interface is provided
either way, and whether it can be exercised is read.
This is not the shape clause 6.2 forbids. An operation that is present and always fails is a defect because the caller cannot tell, and a position the caller reads first is what tells it.
The objection above survives and is answered by clause 9 rather than dismissed: a path few artifacts take is a path little verified, so the conformance arrangement answers that position both ways. Until it did, the unavailable path had no producer at all.
An implementation shall permit concurrent operations upon distinct handles.
Concurrent operations upon one handle shall not damage the implementation's own state. The order in which they take effect, and whether the bytes of one transfer may be separated by those of another, are unspecified.
That permission concerns operations competing for one direction of a resource.
A resource with two independent directions is not a resource with one:
openkal.net states that a transfer waiting in one direction of a connection
shall not delay a transfer in the other or a half-closure, and openkal.datagram
states the same of sending and receiving (version 0.13). Without that statement
an implementation that serialised every operation upon a socket conformed, and a
full-duplex protocol above it waited for ever.
Atomicity below a threshold, which some systems guarantee for some resources, is not required. It is not universally implementable, and requiring it would oblige an implementation to introduce buffering it does not otherwise need.
Handles obtained from the core interfaces are borrowed. They are not released, and no operation releases them.
Handles obtained from openkal.fs and openkal.process are owned. Each of those
interfaces provides the operation that releases one, and an implementation shall
not treat a released handle as valid.
The recommended construction divides the handle word into an index and a generation, incrementing the generation upon release. The specification requires the property and not the construction; this one achieves it without a lookup table, and therefore without the compatibility layer clause 7.1 excludes.
An implementation shall not require a compatibility layer. The test is mechanical: an implementation that must maintain a translation table, a registry, or a name resolver in order to satisfy this specification indicates that the specification has taken a shape borrowed from one environment, and the shape is at fault rather than the implementation.
A handle occupies one machine word and is opaque. An implementation stores whatever it uses natively: a descriptor, an operating-system handle, a pointer to a driver structure, or a capability index.
The width is fixed and the interpretation is not, and this is the property that allows an implementation to be placed above a C library, beneath one, or without one. A handle declared as a descriptor would oblige an implementation whose environment has no descriptors to invent a table; a handle declared as a pointer to a library structure would oblige an implementation with no such library to invent one.
An implementation shall not assume a process model, a division between privileged and unprivileged execution, or a namespace shared by all callers. A handle shall be meaningful in the context of the caller that obtained it.
An implementation shall refuse a word that is not a handle it issued, and shall not act upon it as though it were one it did. The preceding paragraph constrains a handle's scope; this one constrains its validity, and the two are not the same requirement. Under a static link the distinction is academic — the caller and the implementation are one program. Where the implementation is on the far side of a boundary it is the boundary itself, and an implementation that acted upon an arbitrary word would let a caller name a resource it was never given. The division of a handle into an index and a generation, which clause 7.2 already recommends for release, satisfies this as well.
Where the environment already provides an allocator, kal_alloc shall be built
upon it and shall not be built beside it.
The requirement addresses a defect that appears only under load. A C library's formatted output is commonly coupled to that library's own allocator; an implementation that introduces a second allocator therefore places two independent claimants on one region of memory, and on a system whose heap grows by extending a single region the two will interfere.
kal_stream_write shall transfer the whole buffer or report the condition that
prevented it. A partial transfer is not a successful outcome.
The alternative convention, under which the caller inspects a count and repeats the call, places one loop in every caller. Interfaces that adopted it have been a recurring source of defects. The loop belongs in the implementation, which is written once.
kal_stream_read shall report the number of bytes transferred, which may be
fewer than requested. A result of zero denotes end of input. Unlike a partial
write, a partial read carries information the caller requires.
Both report that number, or the negated error value when no byte was moved, in one signed machine word — clause 5.2.1. An operation that moved bytes and then met a condition reports the bytes; the condition is reported by the next call, which is what every consumer of the earlier two-word form already did by hand.
An implementation shall retry an operation that its environment interrupts, and shall not report the interruption. A caller cannot distinguish an interrupted call from a genuine failure without knowledge of the environment, and an implementation that reports it produces short transfers on any system that delivers asynchronous notifications.
The vector supplied to kal_process_spawn is complete: its first element is the
name the started program shall observe as its own, and an implementation shall
pass the vector unaltered. An implementation shall not derive the first element
from the path argument, and shall not prepend, append or reorder elements.
The rule exists because the two sides must agree. A started program reads its
own name through kal_env_arg(0), so a caller that did not supply it could not
predict what the program would read; and the name a program observes as its own
is behaviour on every environment that has an argument vector, so the choice
belongs to the caller rather than to the implementation.
An implementation whose environment has no argument vector shall report
kal_err_unsupported from kal_process_spawn rather than discard the vector.
An environment that gives a started program one string, which the program splits
by rules of its own, passes the vector by quoting each element so that the
conventional splitting recovers it. A program whose rules are not the
convention --- the command interpreter, whose /c takes the rest of the string
as a command line of its own grammar --- cannot be given every argument through
that quoting. KAL_SPAWN_ONE_STRING asks for the first element quoted as usual
and the others joined after it by single spaces, unaltered; an implementation
claims it with KAL_PROCESS_PROP_ONE_STRING, and one whose environment gives a
program a vector reports kal_err_not_supported (version 0.17.1).
This rule was absent from version 0.3, and both implementations existing at the time prepended the path. They agreed only because they shared an author; a third implementer had nothing to consult. The defect was found by writing one program against the specification and running it, which is what clause 9 exists to require.
kal_fs_info shall report a name that does not exist by returning kal_ok and
setting kind to kal_node_absent. It shall not report absence as an error.
Enquiry and access are different operations. A caller that asks what a name refers to has been answered when told that it refers to nothing, and an implementation that reports absence as a failure obliges every caller to distinguish that failure from the ones that denote a broken enquiry — a directory it may not read, a name it may not resolve.
kal_fs_open and kal_fs_open_dir are access rather than
enquiry, and shall report a name that does not exist as kal_err_not_found.
The distinction is the same one openkal.env draws between a variable that is
absent and one whose value is empty, and it is drawn for the same reason: a
caller that cannot tell them apart cannot act correctly upon either.
kal_fs_open takes a flags word. The two-flag form of version 0.3 remains
specified and remains available, and an implementation ordinarily defines it in
terms of the flags form.
The reason for the addition is that three of the conditions a C library must express cannot be reconstructed above the two-flag form without making the caller silently wrong, which clause 3.1 identifies as the boundary between a supply and a simulation.
| Condition | What reconstruction above the two-flag form produces |
|---|---|
| truncate | Opening and then setting the length is two operations. A program that stops between them leaves the tail of a longer previous contents behind, and the file is neither the old one nor the new one. |
| exclusive | Enquiring and then opening is not exclusion. Between the two, another context creates the name, and the caller that asked to be the creator is not. |
| append | Seeking to the end and then writing is not appending. A second writer between the two produces an overwrite, and neither writer observes that it happened. |
kal_fs_truncate and kal_fs_file_info are added for the same reason and are
recorded here rather than in a list of conveniences. Setting the length of an
open file is not expressible through any other operation. Enquiring about an
open file is not expressible through kal_fs_info: the name a file was opened
by may since have been removed or given to another file, so a C library
answering the enquiry from the name would answer about a different file than
the one the caller holds.
kal_exit shall terminate immediately. Registered exit handlers and static
destructors shall not run. An implementation that runs them prevents a caller
from reasoning about what executes after the call.
openkal.task reports, through kal_task_props, whether a context started by
kal_task_start observes the thread-local storage of the toolchain that
compiled the program.
It is reported rather than provided. The register convention that delivers thread-local storage belongs to openarch (clause 10), and an operation here that installed a thread pointer would place a processor's calling convention in an interface that is meant to be independent of it.
The property is reported because a C library ported onto openkal keeps its per-context state --- its error value, its locale, its cancellation state --- in one such variable, and therefore cannot be ported onto an implementation whose contexts lack it. Reporting the property allows the library to state that requirement. An implementation whose contexts are the host's threads has the property without doing anything; an implementation upon a scheduler of its own has it only if it establishes the convention, and one that does not shall report the position as zero rather than leave the question to be discovered.
An interface that reports a property of a resource and offers no way to set it is incomplete wherever the property is one the environment records rather than derives.
kal_fs_file_info reports the time at which a file was last modified.
kal_fs_set_modified sets it. The operation was added in version 0.5 because
three ordinary programs — one that copies a file and preserves its dates, one
that extracts an archive, and one whose whole purpose is to mark a file as
current — could not be written above the interface without it, and each of them
is a program a C library above openkal is expected to host. The absence was not
visible in the specification text; it became visible when such programs were
compiled above an implementation.
It takes the open file rather than the name, for the reason given at
kal_fs_file_info: the name may since refer to something else, and setting the
time of the wrong file is a worse outcome than not setting it.
The three environments openkal has been implemented on record the time to a nanosecond, to a microsecond and to a hundred nanoseconds respectively. The interface states the value in nanoseconds and does not require that it be returned unchanged; the conformance procedure compares whole seconds, which is the resolution every environment that records the time at all agrees upon. An interface that required more would be requiring of every environment what one of them happens to provide.
An implementation whose environment does not record the time at all does not
claim KAL_FS_PROP_MODIFIED_TIME and reports kal_err_not_supported. An
implementation that claims the position is required to perform the operation:
clause 6.2 exists so that a claim is a claim about what can be done.
openkal.fs names things relative to a directory the program holds, and every
operation that answers a question about a thing takes a name. A program holding
a directory therefore had no way to ask a question about that directory: what
it is, when it last changed, whether it can be written. The handle is not a
name, and no operation took a handle alone.
The remedy is one reserved word rather than five more operations. "." denotes
the directory itself, wherever a name is accepted. It does not introduce a way
to ascend, so the confinement clause 7.1 depends upon is unaffected; and every
environment openkal has been implemented on can express it, two of them because
their own naming already reserves the same word and the third because its
object manager expresses the same thing as an empty name beside the directory's
handle.
".." remains invalid. The asymmetry is the point: the first names the thing
the program already holds, and the second names something it does not.
A program started by kal_process_spawn shall receive the three streams and the
directories its caller granted, and no other handle. The requirement includes a
handle the calling program itself received from its own environment: such a
handle was not granted by the caller, and a caller that starts programs inside a
confinement relies on nothing reaching them that it did not name.
Where the environment's mechanism for starting a program that needs an interpreter requires a handle to survive the start, an implementation may leave one. It shall name no more than the program being started, and shall be left only for a program that needs it.
A granted directory is received as a preopen: the started program enumerates,
through kal_fs_preopen, exactly the directories granted, in the order given and
under the names given, and a grant of none leaves it none. The first grant is
the directory the started program regards as the one it was started in, as the
first preopen is for every program. How an implementation conveys a grant is its
own concern; a grant confines a program that uses only its preopens, and the
confinement of one that does not is the environment's responsibility (clause 11,
entry 6).
A granted directory may be an attenuated handle (kal_fs_dir_attenuate,
version 0.23): the started program receives it with its rights and statements,
and reads them in kal_fs_props of its preopen. A caller that sets
KAL_SPAWN_CONFINE asks the environment to hold the started program to its
grants and their rights even where it reaches files without this interface; an
implementation that claims KAL_PROCESS_PROP_CONFINE does so before the program
runs its first instruction, and one that does not refuses the flag and starts
nothing (clause 11, entry 53).
The requirement was descriptive in version 0.13, in clause 11, entry 18, and two implementations departed from it without any observation reporting the departure. One conveyed every inheritable handle of the caller; another conveyed a handle for the directory a program was found in, which reached the whole file system from inside a confinement. Each implementation observes the first paragraph in its own tests, since positions and handle tables are not portable; the conformance suite observes the third.
kal_fs_remove of a file another handle holds open removes the name when the
call returns, on a volume for which the implementation claims
KAL_FS_PROP_REMOVE_OPEN. The open file remains usable through its handles
until the last of them is closed; the directory that held the name is then
empty and may be removed, and the name may be created again.
The behaviour is the one a C library's unlink states, and a program relies on
it without naming it: a lock file held for the program's lifetime, a temporary
file kept open after its name is removed, a tree removed while one of its files
is still open. One environment defers the removal by default --- the name
remains, refusing every open, until the last handle is closed --- and offers
the immediate form on the volumes that record it. Measured above an
implementation that used the deferred form: a C++ program's remove_all of a
directory holding its own lock file failed with "directory not empty" on that
environment alone, while the same program built for the environment removed it.
A volume that cannot remove a held name at once is answered before the call (clause 6.2): the position is not claimed, and a caller that needs the directory gone closes what it holds first.
A position of kal_spawn_streams is inherited, or receives a stream the caller
placed. KAL_SPAWN_NULL_IN, KAL_SPAWN_NULL_OUT and KAL_SPAWN_NULL_ERR give
it the environment's null device: at that position the started program reads
the end of input, and every write is accepted in full and discarded. An
implementation claims the three with KAL_PROCESS_PROP_NULL_STREAMS; one that
does not reports kal_err_not_supported and starts nothing. A flag beside a
non-zero stream at the same position is kal_err_invalid. The device occupies
its position and is not a further handle (clause 7.13), and an implementation
that opens it for the start leaves the caller holding nothing of it.
The device is a resource of every environment that starts programs, and each
names it in its own spelling: /dev/null on Linux and macOS, NUL on Windows.
This specification names nothing outside the directories it supplies, so the
device is reached by the position it is given at and not by a name; a C library
maps its own spelling of the device onto the flags. A library cannot compose the
device from what precedes it: a placed stream is a file's or a channel's end; a
file keeps what is written, and a channel's far end either ends the started
program's writes with an error once its reader is released, or keeps a reader in
the caller for as long as the program runs.
kal_fs_dir_attenuate derives from a directory handle one that reaches the same
directory with less: read-only (KAL_DIR_READ_ONLY), with the holder's
statement that nothing beneath changes (KAL_DIR_UNCHANGING), or recording the
names reached beneath it (KAL_DIR_RECORDED). A derived handle has the rights
of the one it came from less those withdrawn, and the statements of both;
nothing restores a right. A directory opened beneath it has its rights and
statements, and so does a program it is granted to. The rights are held by the
implementation on every operation through the handle; an implementation that
answers from what it read before, under KAL_DIR_UNCHANGING, is answering a
question the holder declared settled. The record is the holder's file, appended
in whole entries by every program holding a handle on it, and ended by e for
each program that ended normally (fs.h). Clause 11, entry 53.
A program's resource preference --- balanced, performance (memory and storage
spent for time) or economy (time spent for memory and storage) --- is stated by
the program's caller (KAL_SPAWN_RESOURCES_*), inherited where nothing is
stated, and read with kal_env_resources. No operation of this specification
answers otherwise for it: each layer of the program, and the implementation,
chooses within itself where it trades one resource for another, and the
environment applies its own policy for memory where it has one. A balanced
program sizes what it keeps by what kal_memory_info reports available.
Clause 11, entry 57.
An implementation claiming KAL_PROCESS_PROP_JOB_OBSERVE shall provide a
caller-owned unit before its external members start, an opaque observation
reference, and the three states unknown, occupied and empty.
kal_process_job_create adopts or establishes the caller's private unit;
subsequent starts use the existing kal_spawn.job field. Observation shall
validate the original native identity. A dead leader, a missing name or the
loss of every native reference is not evidence that all members have ended.
kal_process_job_open obtains observation and close authority only. This is a
caller precondition where the native handle is numeric; it does not convert a
reusable numeric identifier into a kernel-enforced read-only capability.
Before kal_process_job_retire, the caller shall close admission, including
concurrent starts. Retirement proves that no external member remains and,
where necessary, leaves the original unit for the unit of the same live
program identified by continuation. Its identifier and birth are verified
before and after that transition. An inaccessible, dead, reused or unreachable
continuation yields unknown. Retirement conveys no admission or termination
authority over the continuation's unit and never reopens the original unit.
The caller may remain alive to finish its own bookkeeping. Clause 11, entry 58.
Some environments have one task: a firmware's boot services, a disk operating
system, a boot chain. In each, a program that starts another is suspended until
the started program has ended --- StartImage and the EXEC function return when
the image they started returns --- and nothing in the environment can make the
two run together. Such an environment can provide openkal.process, and the
text of this interface did not exclude it; what it did not say is how a caller
learns that the program it started has already ended.
An implementation claiming KAL_PROCESS_PROP_RUNS_TO_END returns from
kal_process_spawn only after the started program has ended. Its status is then
known: kal_process_wait answers it at once, the first time it
is asked. Such an implementation does not claim KAL_PROCESS_PROP_TERMINATE,
for there is no running program to request the end of, nor
KAL_PROCESS_PROP_CHANNEL, for neither end of a channel could read what the
other writes while it runs; a stream passed to the started program is written
by it and read by its starter afterwards, and a pipe's capacity bounds what
passes that way.
A caller that starts a program and then waits for something the program does while it runs --- a line on a channel, a file it creates --- reads this property first. A caller that only starts and waits needs nothing: the two operations mean the same under the property as without it. An implementation that does not claim the property starts a program that runs alongside its starter, as before. Clause 11, entry 61.
Each interface is versioned independently. A revision may add declarations and shall not alter existing ones. Structure layouts are immutable as stated in clause 5.3.
A patch release (the third number) adds only what an implementation may decline:
an optional property, an operation an implementation may refuse with
kal_err_not_supported, or text that states what was already required. An
implementation written against an earlier patch of the same minor version
conforms to it, and the conformance suite's observations of the addition run only
where an implementation claims what they observe. A requirement every
implementation must meet advances the minor version. Version 0.25.1 is the
first patch release under this rule (clause 11, entries 61 and 62).
A consumer declares a dependency upon the specification package, which is the package it imports, and a dependency upon an implementation, which is the package that supplies the definitions. The first fixes the version of the contract; the second is ordinarily conditional upon the target, so that changing implementation is a change to one line of the manifest and to no line of the source.
A conformance suite shall verify both halves of an implementation's claim, and the specification package shall verify the third.
-
Behaviour. Every operation the implementation declares shall behave as this specification requires.
-
Surface. The names an implementation exports beginning with
kal_shall be compared againstSURFACE.txt. An implementation exports the names of the interfaces it provides and no other. This half is a static examination of the artefact and is therefore neither slow nor susceptible to incomplete coverage, and it is the half that detects an implementation which has extended the interface rather than implemented it. -
Declarations. The declarations, clause 4.2, shall be compared against
SURFACE.txtby compiling a translation unit that names every entity the list contains. This half belongs to the specification package rather than to an implementation. It does not compare the two ways of reaching the declarations with each other, because there is one declaration and no comparison to make; what it detects is a name the list requires and the header does not declare. The translation unit is compiled without the environment's headers, because the consumer the header exists for is. -
Types. The declarations shall be examined for the types they are written with, and shall name none that clause 5.4 excludes. This half also belongs to the specification package. It is distinct from the second: an implementation may export exactly the names
SURFACE.txtrequires and still declare one of them taking asize_t, and the surface comparison would report conformance.The examination asks a compiler what it parsed rather than searching the text, so that a type reached through a macro or an include is seen, and the typedef names are read as written rather than resolved to the underlying type — a declaration written
kal_u64and one writtenuint64_tdenote the same type on one target and different types on another, which is the whole subject of clause 5.4.A check that reports conformance by finding nothing reports it identically when it has read nothing. The procedure therefore establishes that the declarations were parsed before it is permitted to report success, and the specification package demonstrates that the check fails when an excluded type is present.
Behavioural conformance cannot be established exhaustively, and this specification does not claim otherwise. An implementation may export the correct names, satisfy every test, and fail on an untested input. The residue is the coverage of the suite, and no arrangement of declarations removes it.
openkal is one of three boundaries and is distinguished from the other two by the number of implementations that coexist within one program.
| Specification | Implementations in one program | Form of the contract |
|---|---|---|
| openarch | one, being the processor | concepts, with link-time optimisation required |
| openkal | one per interface | a C application binary interface |
| openhal | many, being the devices | concepts |
openarch and openhal both express their contracts as concepts, and for opposite reasons. An openarch operation such as disabling interrupts is a single instruction, and the cost of a function call is accordingly prohibitive. An openhal contract must admit several implementations within one program, which a set of C functions cannot. openkal is subject to neither pressure: an openkal operation ordinarily crosses a protection boundary, so the cost of a call is immaterial, and one implementation per interface suffices.
A device supplied through openhal may serve as the implementation of an openkal interface. The relationship is one of connection rather than of layering, and the connection is made by the package that describes the board.
A process holds one implementation of openkal. The table above says one per
interface; this states the unit it is counted in. An implementation keeps state
that belongs to the process and not to a caller --- the word of
kal_process_stop_requested, the table its owned handles index (clause 6.7), the
treatment it installed for the environment's requests --- and two
implementations in one process would each believe they own it. A handle issued
by one is not a handle of the other (clause 7.2), so two parts of one program
that each linked an implementation of their own cannot pass a file between them,
and two treatments of one request to end leave it delivered to whichever was
installed last.
A program linked as one artifact satisfies this by construction. A program
composed of several --- a program and the libraries it loads, a loader and the
images it starts (clause 12) --- satisfies it when the implementation is in one
of them and the others reach it, which is the shared form of an
implementation: an artifact the others load, whose exported names are exactly
SURFACE.txt (clause 9's second half applies to it unchanged), and whose name
carries the major version of the specification it implements, so that two
artifacts written against incompatible versions do not bind to one. An
implementation that offers a shared form states it in its package; it is not
required to offer one, and a program that is one artifact needs none.
What a program cannot do is load a second implementation beside the first and use both. Nothing in this specification detects it, because nothing can: the two are each conforming, and the defect is in the composition. It is recorded here so that a composition is examined for it rather than discovering it as a handle that one half of a program refuses.
The following are recorded so that they are not mistaken for oversights.
-
Concurrency. The behaviour of concurrent operations upon one handle is unspecified. The question becomes unavoidable when
openkal.taskis specified, and version 0.3 does not specify it. -
Ownership. Every handle in the core interfaces is borrowed. Owned handles arrive with
openkal.fs, and the rules for their release, including the effect of releasing one twice, are deferred to that interface. -
Symbol versioning. The C application binary interface carries no version in its symbol names. Clause 8 protects the interface by prohibiting change rather than by permitting coexistence, and an ecosystem that outgrows that prohibition will require a mechanism this version does not define.
-
Readiness. Awaiting one of several sources is not an operation of this specification. It is reached above the interface, from
openkal.taskand a bound upon each wait; clause 6.3 records the alternative that was weighed and the property of implementations that excluded it. -
Name resolution.
openkal.netandopenkal.datagramcarry an address and a port. Turning a name into one is excluded by clause 3.4 and remains so: an implementation shall not be required to parse an unbounded set of name schemes. -
Permission and ownership of files. Not defined, and not a deferral. A permission presupposes an identity, and the environments this specification targets do not agree that one exists.
And what a program should write instead, which was not recorded and is now. A program that means "only I may read this" is stating it in a vocabulary this environment does not have. It has three answers, and none of them is a mode:
- against another part of the same program, the capability already does it: a handle not given cannot be reached;
- against another user of the machine, it is the environment's responsibility — the party that starts the program supplies a preopen that others do not have;
- against a location the program does not trust, it encrypts the contents.
The measurement that settles the alternative: a mode word is not stored by several filesystems a hosted implementation will meet, and on such a volume
chmodreports success and changes nothing, which is the outcome this specification exists to refuse.kal_node_info.writable— one boolean — is not a simplification of a mode word; it is the intersection of what those formats store.Whether a node may be started is not a permission, and version 0.13 defines it.
KAL_INFO_EXECUTABLEandkal_fs_set_executable_atrecord one property of a node, and the property presupposes no principal: a volume that stores it stores it once for the node. It satisfies the criteria this entry applies to a mode word, which the mode word fails. It is answered per volume bykal_fs_props, so a volume that does not store it is asked before it is written to, and an operation upon one refuses rather than reporting success and changing nothing. What remains undefined is every distinction between classes of caller, which is the part that presupposes an identity. -
Creation and reading of links. Settled in 0.9.
kal_fs_link_createandkal_fs_link_readare operations ofopenkal.fs, and they are operations of it rather than an interface of their own for exactly the reason this entry used to give: whether a volume has such nodes is a property of the format and not of the environment, so the variability is between the resources of one implementation. Clause 6.2 says such a property is answered by an enquiry taking the resource, andkal_fs_propsis now that enquiry — which is what makes the operations admissible, because a caller can ask before it calls.Resolution is stated in
fs.hbeside each operation it governs, and no longer only here. It was here alone, and two call sites of one implementation consequently chose opposite directions: opening resolved a link while asking did not, so a program was told that a name referred to a link when opening it would have reached a file. -
Duplication of the calling image.
forkis refused by clause 7.1 and that refusal stands. It is refused as an OPERATION. The atomic capabilities from which a library may compose it are specified:openkal.spaceclones an address space and starts a context in one, andKAL_SPACE_PROP_CLONE_HANDLESstates whether the handles accompany the memory. What this specification declines to do is duplicate execution state, which a library above the interface performs with the compiler's own facilities. A sentence reading "openkal will not have fork" would have buried that distinction, and this entry exists so that it is not written. -
Transfer of a handle between address spaces.
kal_space_startconveys no handle, andkal_process_channelconveys a stream only across a spawn. A general mechanism for passing a handle to a context in another space is not defined by this version. It is the questionopenkal.spacereaches first and is not peculiar to it.What this costs, named in 0.12 because an implementation paid it. A unit is established by whoever starts a program, at the moment of starting it:
kal_spawn.jobis where a caller says which unit a started program belongs to, and the implementation performs the placement. It follows that a program cannot name a unit that a copy of itself went on to form — the copy's handle is the copy's, constructed by clause 6.7 from an index into the copy's own table, and nothing above conveys it back. So the sequence a C library reaches for when it composes job control — duplicate the image, have the copy form a unit of its own, then signal that unit from the original — has no closure here, and this is the reason rather than an omission.The failure this produces is not a refusal, which is why it is recorded. An implementation that meets the dead end is invited to reach for the nearest unit it can name, which is its own; openkal-musl 0.12.0 did exactly that, and every negative identifier that matched no child named the caller's own unit. A program asking after a unit that did not exist was told it did, and one signalling it ended itself together with everything it led. The composition that does work is the one this entry began with: start the program with
kal_spawn.job, where the unit and the caller that knows its name are on the same side of the boundary. -
Exclusion upon a range of a file. Settled in 0.10.
kal_fs_lockandkal_fs_unlockare operations ofopenkal.fs, admitted on exactly the grounds entry 7 records for links: whether a volume can exclude is a property of the format rather than of the environment, sokal_fs_propsanswers it and a caller asks before it calls.And this one is the opposite of entry 6, which is why the two are next to each other. Permission was declined because the environments do not agree that an identity exists. Every environment this specification targets locks a byte range, and spells it almost identically. What was missing was a word, not a capability.
What its absence cost, and it was not a refusal. A C library above this interface answers
fcntl(F_SETLK). With nothing here to answer it with, one returned success and took no lock — two programs held one exclusive lock and neither could find out. Measured against a host. This entry exists so that "a specification with no operation for X" is not mistaken for "consumers of it will simply not do X".Release upon the holder's end is required rather than observed, and that requirement is the whole reason the operation cannot be composed above the line: a caller can build exclusion out of
KAL_OPEN_EXCLUSIVEand a name, and nothing then releases that name when its holder dies. -
A started program that does not outlive its caller. Settled in 0.10 as
kal_process_spawn_bound, and respelled in 0.11 asKAL_SPAWN_BOUND_LIFETIME— see entry 14.Clause 7.1 declines to replace a running image, and that stands. The consequence, which this entry did not previously record, is that a C library asked for
execvecomposes it — and the composition leaves three images where a system with the operation has two: the caller, a copy that waits, and the program.A signal reaches the middle one.
kal_process_terminateupon the identifier the caller holds terminates the waiter; measured with a host as control, the caller is told the program died on the signal it sent while the program runs to completion, unsupervised. The termination operation was not at fault — it was asked to terminate one started program and did. What was missing was a way to say the thingexecvemeans. -
How many contexts run at once. Settled in 0.10.
kal_task_parallelism.KAL_TASK_PROP_PARALLELsays whether and not how many, and a C library above had nowhere else to look:hardware_concurrencyanswered 1 with no error, so a program sizing a pool of workers got one worker and no way to know. Zero means "cannot say" and is distinct from one, because an environment with one processor and an environment that will not answer call for different behaviour. -
The time of a name, and the size of a volume. Settled in 0.10.
kal_fs_set_modified_atandkal_fs_capacity. The first exists becausekal_fs_set_modifiedis stated on an open file while a directory is opened as akal_dir— so this interface had no route to a directory's time at all, and a consumer that stamps a lock directory drove one implementation to open the directory for reading and set the time on that, outside anything stated here. A divergence caused by a missing declaration is a defect of the specification and is recorded as one.The second,
kal_fs_capacity, exists for the reason that keeps it in this interface rather than inopenkal.space: how much room a volume has is a property of the names a caller can already reach, not of the memory a program runs in. A C library above answersstatvfs, and with nothing here to answer it with it reported a fixed number — which is worse than refusing, because a program that checks for room before writing was told there was room.It answers bytes and not blocks, and that is the whole of the design decision. Every environment this specification targets states a block count and a block size, in units of its own choosing, and every one of them differs; a caller wanting bytes multiplies two numbers whose meaning it must first look up. Bytes are what the caller is deciding about, so bytes are what this returns and the multiplication happens once, in the implementation, where the units are known.
-
Starting a program was becoming a family, and 0.11 stopped it. This is the first entry that records a REMOVAL, and it is recorded here for the same reason the additions are: so that the next reader meets the reasoning in the specification rather than in a diff.
0.10 ended with three declarations —
kal_process_spawn,kal_process_spawn_withandkal_process_spawn_bound— which were never three operations. They were one operation and three modifiers of it, spelled apart for exactly one reason: clause 8 forbids adding an argument to a declaration that exists, so each modifier had to arrive as a new name.The 0.10 text on
..._boundpredicted where that ends, in terms: "Declaring every combination is how an interface acquires four spawns and then eight." Two more modifiers then arrived together — the directory a program runs in, and terminating what a started program itself started — and the evidence that they arrive together was a single consumer signature whose child callssetpgid(0, 0)andchdir(cwd)on adjacent lines. Four orthogonal modifiers is sixteen declarations, each written in every implementation.⇒ So the modifiers moved into
struct kal_spawnand the family collapsed to one declaration...._withand..._boundwere removed rather than kept beside it. openkal has no users outside this ecosystem, so there was no one to keep a second spelling for; an interface that offers two ways to say one thing must explain the difference for ever, and this one had no difference to explain. A caller written against the old shape fails to compile — different arity, different types — which is the loud failure and not the quiet one.What this costs, stated rather than glossed.
struct kal_spawnis frozen by clause 5.3, so a future modifier that carries a parameter rather than a flag cannot be added to it and will need its own declaration after all. This entry does not claim to have solved that; it claims that one general form plus a flag word is a better place to meet the problem than sixteen names, and that the modifiers wanted so far are all flags.And clause 8 was set aside to do it, deliberately and once. Clause 8 exists to protect written code, and 0.11 is the last version at which there is none to protect. An equivalent change after this specification has consumers is not permitted by clause 8 and this entry is not a precedent for one.
-
KAL_SPAWN_OWN_JOBhas a shape borrowed from one environment, and clause 7.1 is what says so. Open, and recorded before it is settled — because the flag ships in 0.11 with one implementation refusing it, and a reader is owed the reason.The flag asks that a started program and its descendants form one unit, and
kal_process_terminateend the unit. Two implementations satisfy it with no stored state: a program that calledsetpgid(0, 0)has a group identifier equal to its own, sogetpgid(pid) == pidrecovers the association from the kernel.openkal-windows cannot recover it. It can form the unit — a job object is exactly this — but there is no call there that answers "which job is this process in" with a handle, and
kal_processis one word already holding the process. Satisfying the flag would require a registry keyed by process handle.Clause 7.1 states the consequence mechanically: an implementation that must maintain a translation table, a registry, or a name resolver in order to satisfy this specification indicates that the specification has taken a shape borrowed from one environment, and the shape is at fault rather than the implementation. So this entry is a defect of the flag, not of openkal-windows, and openkal-windows refusing is the correct behaviour under clause 6.2 meanwhile.
⇒ The natural shape makes the unit a resource the caller holds, created before its members and terminated as a unit, so that nothing has to be recovered on either kind of system. It is not settled here because the two kinds of system create such a unit differently — Windows makes it externally and assigns members, while a process group is made by a member from inside — and a shape that is natural to one and not the other is the same defect again, spelled the other way round.
-
A node that may be started, and a start that could not happen. Settled in 0.13. Two consumers of openkal-musl met the same gap from its two sides. A packaging tool that unpacks a program could not leave the result startable, and a C library could not report why a start failed:
ENOEXECarrived above the interface askal_err_io. The first is answered byKAL_INFO_EXECUTABLE,KAL_FS_PROP_EXECUTABLEandkal_fs_set_executable_at(entry 6 states why this is not a permission); the second bykal_err_not_program(clause 5.2 states the criterion it meets).The same examination found that one implementation reported
kal_okand a handle for a start that did not happen, including for a name that does not exist.kal_process_spawnnow states that a start that did not happen is reported, and the conformance suite observes it with a name that does not exist, which requires no file to be created. -
Two directions of one resource. Settled in 0.13; see clause 6.6. The conformance suite observes it with two contexts and a bound, so that an implementation that serialises the directions is reported rather than left waiting.
-
A stream at an arbitrary position of a started program. Considered in 0.13 and not defined. A started program receives three streams, and a descriptor at any other position is not conveyed; version 0.14.1 states this as a requirement in clause 7.13. A position is the shape of one environment's descriptor table (clause 7.1): another environment conveys handles to a started program as a list of values and has no positions, and a started program would still need to be told where to look. Were the need measured, the shape consistent with this specification is the one
kal_preopenhas, a named grant rather than a numbered position. No consumer is silently wrong today: openkal-musl refuses the placement it cannot express. -
A stream handle of zero. Recorded at
kal_spawn_streamsin 0.12 and not changed in 0.13. Zero denotes inheritance there and is a valid stream on an implementation whose streams are descriptors. The specification permits an implementation to remove the ambiguity by never answering a stream enquiry with zero and does not require it; a caller that cannot tolerate the ambiguity reports the request as unsupported, which is what openkal-musl does. -
A keystroke the environment keeps for itself. Settled in 0.14.
KAL_TERM_PASS_CONTROLstates whether the environment reserves any keystroke for an action of its own, and a program that reads what is typed asks for the position and reads the mode back. Admitted on the grounds entry 10 records for locking: every environment this specification has been implemented on reserves such keystrokes and spells the switch almost identically —ISIGand the flags beside it,ENABLE_PROCESSED_INPUT— so what was missing was a word and not a capability. The admission test of clause 7.11 is met by three ordinary programs that cannot be written above the interface without it: an editor, a pager, and a prompt with line editing of its own.What its absence cost, and it was not a refusal. A C environment above this interface answers
tcsetattrwith the mode word. With no position for the reserved keystrokes, a program that cleared line assembly and the echo was still ended by the interrupt key, and no operation had misbehaved: the implementation preserved what the specification had not named. Measured against a host C library upon one terminal, and reported by the consumer who met it.The position is spelled in the sense clause 6.2 now states, so an implementation released before it reads as zero and thereby says something true about itself. The arrangement in which zero would have been the guarantee was considered and not adopted: it required an exception to clause 6.2 and a property position beside the mode position, to repair a hazard that the spelling itself removes.
What one position cannot record. An environment may reserve several classes of keystroke through separate mechanisms of its own, and a terminal upon which some of them had been released is restored to the set the environment ordinarily reserves. A position for each class was weighed and declined: one of the three environments governs the classes with a single switch, so the finer interface would have required of it a distinction it does not have (clause 6.4).
-
The stack a running context stands on. Settled in 0.15.
kal_task_stackreports the region the calling context's stack occupies. It was asked for by the C library this specification is implemented beneath:pthread_getattr_nphad no honest answer above an implementation ofopenkal.task, and the port replaced musl's two wrong answers with a refusal (mcpplibs/openkal-musl#49). A refusal is correct and is still not an answer, and the callers that want one are ordinary rather than exotic — a run-time that keeps its own stacks off the native stack's end, a collector that scans the region it stands on, an interpreter that checks for room before it recurses. Whether the need is measured here is not what admits the operation; clause 6.4 is, and it is answered below.It asks about the caller and takes no handle, and clause 7.2 is why. A handle is meaningful in the context of the party that obtained it, and
kal_task_currentreports an identity rather than a handle — so a context is the one resource its own code always stands on and can never hold. An enquiry taking a context would have to answer for a context the implementation does not control, which is the registry clause 7.1 refuses; addressed this way, the implementation answers about the resource it is running on and records nothing about any other.Every resource answers, which is what clause 6.4 requires. A context that is running can find its own stack in every environment
openkal.taskis provided for, and the three mainstream systems are the evidence: one answers a limit it grows the mapping against, one answers the region it allocated for the thread, and one answers the bounds it built the thread's stack from. The context a program was started on is the easiest case rather than the hardest, because the program's first code is already running on it. An environment that genuinely cannot answer does not provideopenkal.task, which clause 3 already expresses as an absence at the link: a machine with firmware and no operating system provides none of that interface and is unaffected, and an implementation that provides the interface only when it can answer provides it whole, asopenkal-emscriptenalready does by defining the interface under one feature and nothing without it.The size is the region the implementation vouches for, and where an environment separates a reservation from a commitment it is the reservation. A caller places a guard below the region or measures the room above it, and a bound that moved as the stack grew would answer the second question wrongly and the first one not at all. The consequence is stated where it can be met: an implementation that reports the reservation and an implementation that reports only what is committed both conform, and a caller is told which region it has rather than how large the environment would have let it become.
What this costs. A context can be asked only about itself, so the C library above answers
pthread_getattr_npfor the calling thread and refuses every other thread rather than describing one it cannot see. That is a smaller surface than the name suggests, it is uniform across the contexts an implementation did not create, and it is recorded in that port's own table of what it does not supply. The alternative — an answer composed from a record the implementation kept about contexts it does not control — is the shape clause 7.1 excludes, and the defect this entry follows was a wrong range rather than a missing one. -
A least size for a started context's stack. Settled in 0.16.
kal_task_start_sizedstarts a context whose stack is at least the size asked for. Version 0.15 said the size was "a property rather than a parameter" and offered no enquiry that reported it, so it was neither: a C library above acceptedpthread_attr_setstacksize, ran every thread on the implementation's own stack whatever was asked, and could not say how large that was (mcpplibs/openkal-musl#48). The reason given — that an environment which does not allocate stacks separately cannot honour a request — named no environment, and every environmentopenkal.taskis implemented on takes a size natively: two of them as an argument of the call that starts a thread, and the third because the implementation maps the region itself.A least size, and a request beyond what the environment can give is
kal_err_no_memory. Clause 3.1 is why this is a supply: a stack is memory, and an environment that bounds its size bounds what is available, which is the outcomekal_allocreports when it is exhausted. A position inkal_task_propssaying whether requests are honoured was considered and not adopted: it would have admitted an operation that a provided interface answers with a refusal, which clause 6.1 treats as a defect, and there is no environment it would have described. An exact size was considered and not adopted, because the C library above asks for its own default on every thread and that default is smaller than what the implementations beneath it provide.There is no form taking a region, for the reason the declaration gives, and the C library above refuses a thread whose attributes name a region rather than run it somewhere else and report success.
-
Whether input is altered on its way. Settled in 0.16.
KAL_TERM_PASS_INPUTstates that what is typed reaches the program as the keystrokes produced it. Without it the return key reached a program in raw mode as a line feed in two of the three environments, the same byte another key produces, and in the third Ctrl+J produced no byte at all; a program that inserts a line break on one key and submits on the other could not be written (mcpplibs/openkal#48, measured against a host C library on one terminal, and on the third environment's console with the keys written into it as key events). With the position set, all three deliver the return key as 0x0d and Ctrl+J as 0x0a.It is a position of its own and not a widening of
KAL_TERM_PASS_CONTROL. That position was assigned in 0.14 and retains its meaning (clause 6.2): implementations released before this one report it set whenever no keystroke is reserved, and a reading that included the alteration of input would have had each of them claim something about it that it had never established. The two questions are also independent — whether a keystroke is kept, and whether one that is delivered is rewritten — and a program performing line editing of its own wants the second without the first.It is not entry 20's declined finer position. That entry declined a position for each class of reserved keystroke, because one environment governs the classes with a single switch. This position is a different dimension, and it is spelled at the same granularity entry 20 chose: one position covers every mechanism by which the environment alters input.
An environment whose line assembly alters input whatever it is asked reports the position clear while it assembles lines, and a request leaves it so; the program asking is told by
kal_terminal_get_mode. The alterations an environment applies when the position is clear are its own and differ between the three, so the C library above, which knows what its caller asked for, asks for unaltered input in raw mode and performs the rewriting its caller asked for itself. That is how a caller of the C library reads the same bytes on every environment. -
What closing a directory does to its enumerations. Stated in 0.16.
openkal.fssaid from its first version that closing a directory releases the iterators it produced, and no observation reached the case. One implementation released them only at the end, so a C library above that stopped reading a directory early kept a descriptor for every directory it closed, until a program that polled a directory ran out of them (mcpplibs/openkal-linux#34). The declaration now states the consequence for a caller as well: the directory is a released handle, andkal_fs_list_nextgiven it refuses rather than using an iterator the close has released. Each implementation observes the release in its own tests, because a count of open handles is not portable — clause 7.13 observes its first paragraph the same way, for the same reason. -
A rename that does not replace. Settled in 0.17.
kal_fs_rename_flagswithKAL_RENAME_NO_REPLACE, answered per volume byKAL_FS_PROP_RENAME_NO_REPLACE. Three consumers above this interface publish a file under a name only if nobody else has --- a package store, a build cache several programs fill, an installer --- and each composed it from an enquiry and a rename, which loses the name to a second publisher between the two. The composition throughKAL_OPEN_EXCLUSIVEdoes not help: it creates the name and the rename then replaces what was created. Every environment this specification targets performs the operation natively where the volume can, so it is a word that was missing (entry 10's criterion), and a volume that cannot is answered before the call (clause 6.2). A second declaration rather than a flags argument added tokal_fs_rename, because clause 8 forbids altering the first. -
Ending as the request to end would have. Settled in 0.17.
kal_process_end_as_requested. A program that takes the request over withkal_process_stop_requestedand finishes its work had no way to end as the request would have ended it:kal_exitwith any status reports a program that chose to end, a shell running a loop then starts the next program, and a starter that waits cannot tell a cancelled build from a failed one. The report it wanted is the environment's own report of an end the environment caused, which nothing above this interface can produce. Measured by the consumer that met it: a build tool's cancelled run was reported to its shell as an exit status, and the shell's loop continued.The take-over is stated with it, in
process.h: from the first call ofkal_process_stop_requested, a request reaches the program only through the word, and a request its starter arranged not to deliver stays undelivered. Two implementations restored the delivery of a request their starter had declined, which ended background programs that were meant to survive it. -
A rendezvous at a name. Settled in 0.17.
kal_net_listen_atandkal_net_connect_at, answered per directory byKAL_FS_PROP_LOCAL_ENDPOINT. Two programs on one machine --- an editor and the server it starts, two instances of one program deciding which is the first --- met at an address on the loopback, which every program on the machine can reach, so each protocol carried its own access control. A name in a directory is reached by a program that holds the directory, which is the confinement of clause 7.13 applied to a connection. Every environment this specification targets binds such a connection to a name natively. It is admitted as two operations ofopenkal.netrather than an interface of its own because the resources are the ones the interface already has --- a listener and a connection --- and only where they meet is new. Passing a handle across such a connection is not admitted, for the reason entry 9 gives. -
Publishing part of a region. Settled in 0.17.
kal_exec_publish_partandkal_exec_granularity.exec.hdeclined a partial form on the ground that it would report the environment's granularity through the shape of the interface, and said in the same header that a loader belongs above openkal and needs only this interface. The two statements could not both hold: an image's instructions and data lie at fixed distances in one region, so a loader must make the first executable and leave the second writable. Measured by writing the loader (a C library'sdlopenabove three implementations). The granularity is reported by its own operation because it is notkal_memory_granularity: one environment reserves in sixty-four kibibytes and protects in four. -
What is known of a running program. Settled in 0.17.
kal_process_self,kal_process_open_idandkal_process_info, withkal_program_infoin the shape ofkal_node_info. A language server is told its client's identifier and must notice when the client is gone; a lease names the program that holds it; a bound on the processor time a program may use is checked against what it has used. None could be answered above this interface, and the consumer that needed all three kept seven declarations of one environment's functions and read another's process table by name. The identifier is opaque and is given again, so it is reported with the moment the program started, and the pair is what names one program. A program the caller did not start can be asked about and not waited for or ended: the second would end a program by a number that may since name another. -
The image a program was started from, and the system it runs upon. Settled in 0.17.
kal_env_imageandkal_env_system, andkal_env_loaderfor an image (clause 12): what starts the same program again is a loader and an image there, and the loader is named nowhere else. The first is a fact every environment that starts named images tells the program and that the program cannot recover otherwise; argument zero is the starter's choice (clause 7.6). The second exists for clause 12: a program built once and run upon several systems must still follow each system's conventions --- where a user's configuration is kept, the suffix of a program, the separator of a list of directories --- and those are the system's decisions rather than resources of this interface. It is restricted to conventions in its declaration, because a name of a class of environment used to decide what an implementation can do is the error clause 3.3 records; capability words answer that, about the implementation in front of the program. -
Whether written control sequences are obeyed. Settled in 0.17.
KAL_TERM_SEQUENCES, a mode position of the stream a program writes. A program that colours its output could not tell an environment that obeys ECMA-48 from one that prints it, and one implementation wrote bytes to its console that the console then displayed. ECMA-48 is a standard of its own and not the shape of an environment; one environment of the three obeys it only once asked, and the position is how a program asks. -
The direction of a channel, and the form of an environment entry. Stated in 0.17.
kal_process_channelnamed no direction and both implementations mademinethe reading end; an environment entry with no=was refused by one implementation and passed on by another. Both are now stated inprocess.h, and the second is refused by all. -
One implementation in one process. Stated in 0.17, clause 10.1, with the shared form that lets a program of several artifacts satisfy it. It was assumed rather than stated, and became a question when a loader above this interface began starting images that themselves call
kal_*(clause 12). -
Images. Specified in 0.17, clause 12. Until this version a program above openkal was portable in its source: the same source, built once per environment. The clause that follows makes it portable in its bytes for one architecture, and states why that needed no operation that did not already exist except the three of entries 28 and 30.
-
A directory supplied after inception. Stated in 0.17, at
kal_fs_preopen. The set of supplied directories was fixed when the program started, and on the environment whose volumes are separate roots a volume mounted later could not be reached by any name. An environment may now append such a directory; a position once given keeps its directory and its name, so nothing a program holds changes meaning. The same entry records the root of a network's names, which that environment now supplies as a directory of its own (//). -
An image's entry, and an import it can do without. Stated in 0.17, clause 12.2 and 12.4. An image's entry takes nothing and returns its status, and the loader ends with that status after the finalisers: the arguments are
kal_env_arg's, as in every other arrangement. A weak import is bound when it is provided and is zero otherwise, and it does not enter the requirements note --- so a C library can keep a table of operations it uses when present (adlopenbeside the image, an interface a later version adds) without making every image refuse to load where they are absent. -
Removing a name that is held open. Stated in 0.17, clause 7.14, answered per volume by
KAL_FS_PROP_REMOVE_OPEN. The specification said what a removal removes and not when; two environments removed the name at once and the third when the last handle closed, and a tree holding an open file was removable on two of them. The immediate form is native on every volume that records it, so it is a statement of what was already the common behaviour, and a volume without it is answered before the call. -
Ending a read that waits. Stated in 0.17, at
kal_net_shutdown. Ending the read direction of a connection ends a read waiting in it, with the end of input. Two environments did so and the third left the read pending, so a program that ended a connection from another context waited for its reader there alone; the release of the connection is not the remedy, since a handle in use is not to be released (clause 7.2). -
A command interpreter's command line. Stated in 0.17.1, clause 7.6, with
KAL_SPAWN_ONE_STRING. A program that ran a person's command through the command interpreter of an environment that passes one string had no way to give it the command: the quoting that recovers a vector put escapes the interpreter does not read into every command that held a quotation mark, which a path with a space in it does. -
A part of a file in the address space. Specified in 0.18, as the optional interface
openkal.map(map.h). Three consumers needed it and none could compose it: a compiler run as an image read every file it opened into memory because its C library'smmapof a file answeredENOSYS, and kept a private copy of pages a native program shares; the reference loader read each image whole into a buffer it never released; and two programs that share a region of data (a message bus, a model's weights) had no means at all, since reading is copying. Every environment this specification targets maps a file in one call, so it meets the admission criterion of version 0.8. It is an interface of its own rather than operations ofopenkal.fsbecause an environment can keep files and be unable to map them (clause 6.2); the region is an address and a length, as an allocation is, because a handle would oblige an implementation to keep a table (clause 7.1); an offset is a multiple of a constant rather than of an enquired granularity, because a file is laid out for mapping by a program that does not know where it will be mapped (clause 6.5's second paragraph); and no region of a file is executable, because instructions run only from memoryopenkal.execpublished. A file shortened beneath a shared region refuses the shortening, or faults or reads zero beyond its new end, whichever the environment does, and no access reads the bytes that were removed; a private region may go on showing what it showed, as for any change to its file. -
Physical cores, and a context's claim on the processors. Specified in 0.19, as
kal_task_coresandkal_task_set_priorityinopenkal.task(task.h). A compiler run as an ok image prepared one module at a time where the same machine's own program prepared two: it sizes heavy work by physical cores, its C library had nowhere to ask, and the answer "unknown" became one. And its background indexing competed with what its user waited for, which on its own system it marks as background work. Every environment this specification targets states its cores and lowers a thread's claim in one call each, so both meet the admission criterion of version 0.8. They are operations ofopenkal.taskbecause they are about the contexts that interface starts, askal_task_parallelismis (entry 12). Zero cores is "cannot say", as zero contexts is. Three claims rather than a number, because a number would oblige an implementation to map a scale onto environments that have none (clause 7.1), and three are what Linux's scheduling policies, macOS's quality-of-service classes and Windows' background mode can all say. A context that lowered its claim may be refused the raising of it, which iskal_err_permission; an environment without the operation does not claimKAL_TASK_PROP_PRIORITY. -
A transfer at an offset. Specified in 0.20, as
kal_fs_read_atandkal_fs_write_atinopenkal.fs(fs.h). A C library above this interface composedpreadfrom a seek, a read and a seek back --- three calls where one suffices, under a lock of its own so that a second context could not move the position in between, and still wrong for a sequential transfer the same file was making. Measured on a compiler run as an image: 66 thousand reads and 71 thousand seeks where the same program built for its environment made 648 thousand positional reads and no seeks, and the lock's waits were a further 73 thousand calls. Every environment this specification targets transfers at an offset in one call, so both meet the admission criterion of version 0.8. One of them moves a handle's position when a transfer on it is given an offset; an implementation there transfers at the offset through a second handle to the same file, opened when it is first needed, whose position nothing observes --- so the position a program and the programs it gave the file to share is the environment's own, as it is elsewhere (clause 1.1). Neither operation uses or changes the position; where a write at an offset lands on a file opened for appending is the environment's, and is named rather than reconciled, because reconciling it would oblige every implementation to remember how each file was opened (clause 7.1). -
What a unit uses, and a limit on it. Specified in 0.20, as
kal_process_job_usage,kal_process_job_limitand the memory ofkal_program_infoinopenkal.process(process.h). A program that starts workers in a unit (entry 15) bounds what they may use together, so that a burst of work does not take the machine: a language server on one environment placed its workers in a control group with a memory ceiling and on the others ran them unbounded or not at all. The three environments state a program's memory in one call each, and hold a set of programs to a ceiling in two ways: one family of systems lets a program create such a group where it has been delegated the right, the other places a ceiling on a job object it already uses for the unit, and the third has no such group. So the specification states the limit and names the two ways it can be held:HARD, where the environment holds it, andSUPERVISED, where the implementation observes the unit at an interval it states and ends it on seeing the excess. Both end the WHOLE unit, because a unit ends together; an environment whose own ceiling ends one program or fails an allocation is brought to that outcome by its implementation. Which memory is counted is the environment's (resident, footprint, committed), stated by the implementation, because a common definition would have to be computed from something no environment reports (clause 7.1); what is common is the unit, a byte, a measure that does not change between calls, and, for one program, a peak that never falls. A unit's usage counts every program in it, which every environment records (a control group, a process group, a job object), and states no peak, because only one environment records a unit's peak and reporting one elsewhere would oblige an implementation to remember every unit (clause 7.1).kal_process_waitreports a program a limit ended withterminatedset toKAL_TERMINATED_LIMIT, which a caller that tested for non-zero already treats as an end it did not choose. -
A range of the address space reserved and backed as it is used. Specified in 0.20, as the optional interface
openkal.region(region.h). A C library above this interface built its allocator onkal_alloc, which obtains memory usable at once and returns it whole: every group of small allocations freed became a release, and its next use a fresh request whose pages were faulted in and zeroed again --- 792 thousand faults where the same program built for its environment took 55 thousand. What an allocator of a general kind needs is the other half of an address space: a large aligned reservation, backing a part of it as it is used, returning the backing while keeping the addresses, and releasing the whole. Every environment with a page table does each in one call, so the five operations meet the admission criterion of version 0.8. It is an interface of its own rather than operations ofopenkal.memory, whose place in the core set (clause 3.2) would require them of an environment without a page table, which could only refuse them (clause 6.1). Two returns are distinguished because environments distinguish them and an allocator uses both: afterkal_region_decommita part is backed again only bykal_region_commitand then reads as zero; afterkal_region_discardit stays usable and its contents are unspecified, which lets an environment take the memory back lazily.KAL_REGION_PROP_OVERCOMMITstates the environments in which a reservation is usable at once and backing is not charged apart from it. No protection is stated, as none is anywhere in this specification. A C library builds its anonymousmmap,munmapandmadviseupon it, which is the test that the operations are general rather than one allocator's. -
What a program states to a measurement, and the name of an address. Specified in 0.20, as the optional interfaces
openkal.prof(prof.h) andopenkal.debug(debug.h). A program run as an image could be measured only as an anonymous region: a profiler of its environment saw addresses it could not name, and nothing told it what the program's layers were doing --- how often the allocator went to the environment, how long a context waited for a lock. A loader holds what is missing (it binds every call across this interface, and it read the image's symbol table), so the two interfaces are a loader's to provide (clause 12.3).openkal.profis designed to cost nothing unmeasured:kal_prof_activeis asked once, a counter is the program's own cell read by the measurement when it records, and a phase is a pair of marks.openkal.debugnames an address from the symbol table; it does not walk frames, because a program unwinds itself from tables it already holds (clause 12.2), and the name is returned as the table states it. Neither carries anything of the environment (clause 1.1). A cycle counter was considered foropenkal.profand not adopted: it would be a second time base besidekal_time_monotonic, which a measurement already has. -
Instructions from a file. Specified in 0.21, as
kal_exec_map_part,KAL_EXEC_MAP_EXECUTEandKAL_EXEC_PROP_FILEinopenkal.exec(exec.h). A loader copied every segment of an image into the region it reserved, so each start of a program wrote and made resident the whole of its instructions and read-only data: measured for a language server, 54 MB and about 17 ms before its first instruction, paid again by each of the 176 programs a build of its modules starts, where a program of the environment maps its file and pays for the pages it touches. An image's instructions and read-only data are the file's bytes unchanged (clause 12.2 allows no text relocation). It is an operation ofopenkal.execand not a region ofopenkal.mapthat may be executed, because instructions run only from memoryopenkal.execprovides (entry 40): it is a second way to obtain that memory, and a part made a file's is never writable, so no part of a region is both. It is a part of a reserved region and not a region of its own because an image's segments lie at fixed distances (entry 28). It is a position and not required, because one environment makes a part of a reservation a file's view only where the reservation was made of placeholders, which would make every region a table of pieces (clause 7.1). With it, the processor time of an ended program is stated to remain while its handle is held (kal_program_info), as its peak memory does since 0.20: every environment keeps it in the record of the ended program, and a C library'swait4reports the two together. -
The null device at a position. Specified in 0.22, clause 7.15, as
KAL_SPAWN_NULL_IN,KAL_SPAWN_NULL_OUT,KAL_SPAWN_NULL_ERRandKAL_PROCESS_PROP_NULL_STREAMSinopenkal.process(process.h). A language server run as an image started each worker that builds a module with its standard input and output sent to/dev/null, as LLVM'sExecuteNoWaitsends an empty redirection; on the one environment whose preopens are volumes and not a root, the C library found no such name, and every start failed withENOENT. The device is the environment's and not the library's: a started program needs a handle of the environment at the position, which a library could otherwise supply only as a channel drained by the caller for as long as the program runs. It is a flag of the start and not a stream a program opens, because a stream would need a release of its own besidekal_process_channel_close, and a program that reads or writes the device itself is served by its C library without the environment. It is a position and not required, because an environment that starts no programs has nothing to give it to. -
An image placed at its base. Specified in 0.23, as
kal_exec_alloc_atandKAL_EXEC_PROP_PLACEinopenkal.exec(exec.h), with the placement note (clause 12.4, version 1.1) and its use by a loader (clause 12.3). An image is position-independent (12.2), so a loader wrote, on every start, each address its relative relocations compute: measured for a language server's image, 164 000 relocations over two mebibytes of read-only-after-relocation data, made resident in each of the 176 programs a build of its modules starts --- about 900 page faults a program more than the same server built for its environment at one address, which on a virtual machine of a continuous-integration service cost a fifth of the build's processor time. An image computed once for one base and placed there is the file's pages, as such a program is. The reservation at an address is an operation and not a hint tokal_exec_alloc, because a loader that is refused must know it before it writes anything; it is a position, because an environment may decide where every program's memory lies; and the address is the image's, chosen when it is sealed, not the environment's. -
A name with every link followed. Specified in 0.23, as
kal_fs_resolved_nameandKAL_FS_PROP_RESOLVED_NAMEinopenkal.fs(fs.h). A C library'srealpathasked about each component of a name in turn whether it is a link: measured in a language server's session run as an image, half a million enquiries, where the program of its environment asked the environment once for each file it opened (/proc/self/fdon one system,F_GETPATHon another). Every environment that keeps links answers the question for a name it has resolved, and nothing above it can compose the enquiries into one. The name is absolute and in the environment's spelling, askal_env_imagestates one: a C library maps it onto its own names as it maps that one. It is a position of the volume, as the other operations of names are. -
A region is zero when it is returned. Stated in 0.23 of
kal_exec_allocandkal_exec_alloc_at(exec.h); every implementation gave fresh pages before. A loader that wrote the part of a segment beyond its file's bytes made every page of it resident before the program touched one; measured for a language server's worker, 780 KiB of zeros written on every start. -
A part sealed read-only. Specified in 0.23, as
kal_exec_seal_part,KAL_EXEC_SEAL_FOREVERandKAL_EXEC_PROP_SEAL_FOREVERinopenkal.exec. A loader relocated an image's read-only-after-relocation data and left it writable: its global offset table and its tables of virtual functions, the words an attacker who can write one word overwrites, where a program of the environment has them made read-only by its loader. Sealing for ever is a position, because the environment decides whether a protection can be made permanent. -
A part read in at once, a part written privately, and branch targets enforced. Specified in 0.23, as
KAL_EXEC_MAP_PREFETCH,KAL_EXEC_MAP_WRITE,KAL_EXEC_MAP_BTIandKAL_EXEC_PROP_BTI. The first gathers the page faults a program takes on its first touch of each page of a part into one request: where a fault is dear --- a virtual machine, measured at 24 microseconds each --- the pages an image states it touches when it starts (clause 12.4, type 4) are read in with the part. The second makes a part the file's bytes until the program writes them, each page it writes its own copy and none executed: an image's data at its base (type 3) is then the file's pages, shared by every program and resident where it is touched, as the data of a program of the environment is, where a copy made the whole of it every program's own. The third maps instructions built with branch-target marks so that an indirect branch landing elsewhere faults, where the environment enforces it. -
A handle with less, and a program held to its grants. Specified in 0.23, as
kal_fs_dir_attenuate,struct kal_dir_rights,KAL_DIR_*andKAL_FS_PROP_ATTENUATE,_READ_ONLY,_UNCHANGINGinopenkal.fs, andKAL_SPAWN_CONFINEandKAL_PROCESS_PROP_CONFINEinopenkal.process(clauses 7.13 and 7.16). Three needs met by one operation upon a capability. A language server's engine probed, in each of its workers and for each compile command, the compilers installed on the machine: three hundred thousand failed enquiries in a session, which a program granted only the directories its build names answers in its own C library without asking, and which a tree its holder states does not change lets an implementation answer from what it listed. A tool that decides when a cached result is stale needs every name a program reached, including those it found absent, which the program's own report of its inputs omits: a header added earlier on a search path changes the result and appears in no list of what was read. And a program that parses what it was given --- a source file of a repository it did not choose --- is held, if subverted, to what its caller granted. The record and the statement are attributes of the handle, not of a runner: the environment answers them, whatever runs the program. Confinement is the caller's request and not a consequence of granting, because a program built for its environment needs directories it was not granted. -
A listing with attributes. Specified in 0.23, as
kal_fs_list_next_infoandKAL_FS_PROP_LIST_INFO. A program that lists a directory and asks of each entry what it is crossed this interface twice for each entry; two environments return a batch of entries with all of their attributes in one request, and a kernel that implements this interface crosses its boundary once for both. The same version states what the end of a listing reports ---kal_ok, the iterator zero and the name empty --- which one implementation had reported askal_err_not_found. -
Page faults counted. Specified in 0.23, as
faults_minor,faults_major,KAL_PINFO_FAULTSandKAL_PINFO_MAJORappended tostruct kal_program_info. Where a fault is dear, the faults of a phase and not its processor time are what a phase of starting a program spends; one environment counts only the sum, which is stated. -
A fault reported, and samples and touches taken, by the environment. Specified in 0.23, as
kal_debug_propsandKAL_DEBUG_PROP_FAULT_REPORT, andkal_prof_props,KAL_PROF_PROP_SAMPLES,struct kal_prof_sampleand the three sampling operations, andKAL_PROF_PROP_TOUCHESand the three operations that record the order in which a program first executes the pages of a part of a region; an implementation providesopenkal.debugandopenkal.profto a program built for its environment from this version. Interrupting a running context and reading where it stands needs a timer delivered to it or another context that may suspend it, and the first execution of a page is a fault the environment takes: only the environment has either. The environment's own profilers do not name an image's addresses on two of the three systems, and the party that ends a program for a fault is the one that can still say where. Frames are those of the chain of frame pointers, which the platform's build keeps. A loader above this interface measures touches through it, and does not install handlers of its own. -
A preference about cost, and what is available. Specified in 0.23, as
KAL_SPAWN_RESOURCES_BALANCED,_PERFORMANCEand_ECONOMYinopenkal.process,kal_env_resourcesinopenkal.env, andkal_memory_infoandstruct kal_memory_statusinopenkal.memory(clause 7.17). Each layer of a program holds caches and reserves whose size trades memory or storage for time --- a loader's prepared images, a C library's free memory kept for reuse, an implementation's answers --- and each had chosen one point for every program. The party that knows what a program is for states it once; a balanced program adapts to what is available. It is a preference and not a limit, and no operation answers differently for it, askal_task_set_prioritychanges no result. -
A declared unit whose external writers have ended. Specified in 0.24, as
kal_process_job_create,kal_process_job_open,kal_process_job_observeandkal_process_job_retire(clause 7.18). Termination of one program does not establish termination of its descendants. A retained native reference can establish the original unit's state without a process registry; when the environment cannot establish that identity, the answer isunknown. A helper is itself a member, so successful retirement first excludes external members and then leaves its original unit where the environment requires it. A live continuation names the surviving program, not a remembered numeric group. The existing spawn form and immutable structure layouts remain unchanged. -
A second name for a file. Specified in 0.25, as
kal_fs_hard_linkandKAL_FS_PROP_HARD_LINKSinopenkal.fs. A C library'slinkandlinkat, and a C++ library'sstd::filesystem::create_hard_link, could not be written above this interface: no operation gave an existing file a second name, and copying the file is a different file with a different identity. Every environment this specification targets has the operation natively where the volume's format records several names for one node (linkat,CreateHardLinkW), so it is answered per volume bykal_fs_props, as the operations upon links are (entry 7), and a volume that does not claim it refuses the request rather than copying. The name being given another is not resolved, for the reasonkal_fs_removeandkal_fs_renameact upon a name: a program that names a link means the link. -
The requests to end, counted. Specified in 0.25, in
kal_process_stop_requested. The word was set once and never cleared, so a program that took the requests over and went on after the first could not learn of a second: an interactive program to which each interrupt keystroke is a request to abandon the current command observed the first keystroke and none after it. The word now counts the requests, skipping zero when the count wraps, so the earlier statement --- zero until a request, non-zero afterwards, never cleared --- remains true of every implementation of 0.25, and a program written against 0.24 reads it as before. -
A started program that runs to its end first. Specified in 0.25.1, as
KAL_PROCESS_PROP_RUNS_TO_ENDinopenkal.process(clause 7.19). Three environments the ecosystem prepares an implementation for --- a firmware's boot services, a disk operating system and a boot chain --- have one task, and in each the starter of a program resumes when the started program returns. The interface's text did not forbid such an implementation, and the concurrency the other properties describe is already optional; what was missing was a way for a caller to know. The property is the inverse of the behaviour every earlier implementation has, so no implementation of 0.25 changes by it, and an implementation that does not claim it is read as before. -
Thirty-two bit targets. Stated in 0.25.1 (clauses 5.5 and 12.2). The declarations were already written from the compiler's word and fixed-width types (clauses 5.1 and 5.4); what a thirty-two bit target requires of an implementation --- counts in a word, layouts the target's, handles that fit, time through the environment's sixty-four bit forms --- and what an image of such an architecture is, were not written down. No declaration changes.
-
The surface the conformance suite links. Changed in 0.25.2; no clause and no declaration changes. A link that discards definitions nothing reaches kept of an implementation only what the suite calls, and the comparison of clause 9 then reported an interface absent whose definitions the implementation holds. The suite now reaches every name without requiring it, so a defined name is kept and an absent one is null.
A program written against this specification is built for an environment: its implementation is linked into it (clause 4.4, S), or loaded beside it (L). This clause specifies a third arrangement, in which the program is an image --- one artifact per processor architecture --- and a loader running in the environment holds the implementation, maps the image, binds the names it imports to that implementation, and transfers control to it. The same bytes then run in every environment of that architecture for which a loader exists.
It is an arrangement and not a requirement: an implementation is not required to
be reachable from an image, and a program built for its environment is
unaffected by this clause. The format is called the ok format (openkal
image format); an executable image is conventionally named *.oke, and an image
that is loaded by another *.okso.
An environment's kernel executes only its own format --- each recognises a different header at the start of the file --- and a file cannot be all of them. What is above the kernel is a loader in every environment, and a loader can be written for any format. An image is therefore never started by the environment directly. It is started by a loader that is itself a program of the environment, or by a program of the environment that carries the image within it and starts it the same way.
Nothing else is environment-specific. An image calls only kal_*, so it needs
from the loader only what this specification already declares, together with
the operations a loader needs and a program could not otherwise reach
(clause 11, entries 28, 30 and 46).
| Item | Requirement |
|---|---|
| container | ELF, position-independent (ET_DYN), little-endian, of the architecture's class; no text relocation |
| alignment | every loadable segment aligned to at least 65536 bytes, which is the largest protection or reservation granularity among the environments this specification targets, and KAL_MAP_ALIGNMENT; a loader publishes each executable segment with kal_exec_publish_part, or makes it the file's with kal_exec_map_part |
| relocations | only the architecture's relative, absolute, global-data and procedure-linkage relocations, and the packed relative form; a loader refuses any other |
| imports | only names this specification declares, and names other images export; a name of a C library or of an environment is refused. A weak import is optional: it is bound when the implementation or an image the image needs provides it, and is zero otherwise |
| entry | an executable image's ELF entry is a function of no parameters returning int, in the image's convention; its value is the image's status. An image that another loads has no entry |
| calling convention | the architecture's, fixed for every environment: the System V convention on x86-64, the procedure call standard on AArch64, the System V i386 convention on i386 (arguments on the stack, the stack aligned to sixteen bytes at a call, the result in eax, or edx:eax for sixty-four bits); an implementation whose environment uses another presents entries in this one to the image (12.3) |
| floating-point state | the convention's at the entry and in each context: on x86-64 the x87 control word 0x037F (extended precision, every exception masked) and MXCSR 0x1F80, on AArch64 FPCR zero, on i386 the x87 control word 0x037F and, where the processor has SSE, MXCSR 0x1F80; an image's C library computes in that state (a long double of x86-64 and of i386 is the x87's extended format) |
| architecture | an image is of one architecture and runs on a loader of that architecture: an i386 image (ELFCLASS32, EM_386) on an i386 loader, never inside a loader of another (clause 5.5) |
| platform register | an AArch64 image does not use x18, which two environments reserve |
| stack | a frame larger than a page is touched page by page, in order: one environment commits a context's stack as its guard page is touched, and a frame that steps over the guard ends the program |
| thread-local storage | through the emulated form, in which a variable is found by a function the image itself supplies; an image does not address the thread pointer, which one environment does not let a program set and another does not let it change |
| unwinding | exceptions and unwinding do not cross from an image into a kal_* operation; the interface is a C interface (clause 1) |
| note | an OPENKAL note (12.4) |
The thread-local storage of an image is kept by the image's own C library,
above kal_task_current. kal_task_current names a context only while it runs,
so the library releases what it kept for a context when a context it started
ends; a context the library did not start --- the first, and any an embedding
program started --- keeps its storage for the life of the image. No operation is
added for it. The cost is a function call where a register would have been read;
it is stated rather than hidden, and a program for which it matters is built for
its environment.
A loader holds the process's one implementation (clause 10.1). It maps the image
into a region from kal_exec_alloc, applies the relocations, binds each imported
name to that implementation's entry for it, publishes the executable segments,
runs the image's initialisers and transfers control to its entry; when the entry
returns, the loader runs the image's finalisers and ends with the status the entry
returned (kal_exit). The image's
kal_env_* operations answer for the image: arguments as the loader received
them after its own, kal_env_image the image's name, and kal_env_loader the
loader's own, so that the program can be started again (a loader, then an
image).
Where the implementation sets KAL_EXEC_PROP_FILE (version 0.21), a loader
makes each segment that no relocation writes the file's bytes
(kal_exec_map_part) rather than a copy of them: the instructions executed and
the read-only data read. A program then starts as a program of its environment
does, paying for the pages it touches; a loader copies where the position is
zero, where the image's bytes are not a file's at a multiple of
KAL_MAP_ALIGNMENT (an image a program carries in its own bytes), and where a
relocation writes the segment.
Where the implementation sets KAL_EXEC_PROP_PLACE (version 0.23) and the image
carries a placement note (12.4), a loader asks for the image's span at the
note's base (kal_exec_alloc_at). Given it, the loader takes each stored
segment's bytes instead of the file's and applies only the relocations that
name a symbol: a part of a stored segment that is read-only after relocation
(the image's PT_GNU_RELRO) and that no such relocation writes is the file's
bytes (kal_exec_map_part) where KAL_EXEC_PROP_FILE is set, and the rest is
copied. Refused, or where the position is zero, the loader reserves the span
anywhere and relocates the file's segments as before. The program observes the
same memory either way; what differs is that a program placed at its base writes
no address its relative relocations compute, as a program of its environment
linked for one address does not.
Where an environment's own convention differs from the image's --- on x86-64,
one environment passes arguments in other registers --- the loader presents each
entry in the image's convention, and a function the image passes to the
implementation (kal_task_start's entry, kal_space_start's) is entered in the
image's convention. The convention includes the floating-point state (12.2): the
loader establishes it at the entry, for the initialisers and finalisers, and in
each context the image starts, and a call that a program of the environment makes
into an image's function is made in it, the caller's own restored after it; one
environment begins a thread with the x87 at double precision. The presentation is
mechanical and is generated from
SURFACE.txt and the declarations, so that it cannot omit a name. It is the
loader's and not the implementation's: an implementation is written in its
environment's convention, as every program of that environment is, and a
convention of images is a fact about images.
A loader may provide openkal.prof and openkal.debug to the images it binds,
beside the implementation's interfaces (version 0.20, clause 11 entry 45); from
version 0.23 an implementation also provides them to a program built for its
environment, and a loader forwards an image's samples to its own
implementation (clause 11, entry 56). Both
are about the program as the loader placed it --- its calls, its counters, its
symbol table --- and the loader is the party that holds those, so this is not a
second implementation in the process (clause 10.1): neither interface has a
resource of the environment, and neither keeps state an implementation keeps.
The image's kal_interfaces then answers the implementation's positions and the
loader's together. A loader that measures counts each call an image makes
across this interface by binding the image's import to a counting entry instead
of the implementation's; one that does not binds the implementation's entry, so
a program that is not measured pays nothing.
A loader does not write the part of a segment beyond its file's bytes: the
region is zero (clause 11, entry 50), and a page of it the program never touches
is never made resident. Once it has applied an image's relocations, it seals the
image's PT_GNU_RELRO (kal_exec_seal_part), for ever for an image it will not
unload (entry 51). Where an image states the pages it touches when it starts
(12.4, type 4), it maps those parts with KAL_EXEC_MAP_PREFETCH; where an
image's program property note states that every object carries branch-target
marks, it maps the instructions with KAL_EXEC_MAP_BTI (entry 52).
An image carries notes whose owner is OPENKAL, in a section .note.openkal:
| type | descriptor |
|---|---|
| 1, requirements | kal_u32 major and kal_u32 minor version of this clause (1 and 1; 1 and 0 before version 0.23 added the placement note), then a kal_u64 of KAL_IFACE_* positions: the interfaces the image requires, derived from the names it imports other than weakly |
| 2, signature | 32 bytes of an Ed25519 public key, then 64 bytes of the signature, over the whole file with these 96 bytes read as zero |
| 3, placement | a kal_u64 base, a multiple of 65536; a kal_u32 count and a kal_u32 zero; then, for each of count writable loadable segments that have bytes in the file, in ascending order of address, three kal_u64: the segment's address in the image, the file offset of its stored bytes, congruent to that address modulo 65536, and their length, the segment's size in the file. The stored bytes are the segment's file bytes with each relative relocation, and each of the packed relative form, applied for base; a relocation that names a symbol is not applied. Version 1.1 of this clause |
| 4, touch | a kal_u32 count and a kal_u32 zero; then, for each of count ranges, two kal_u32: the first 65536-byte block of the range in the file and the number of blocks, in the order the image first touches them when it starts. Version 1.1 of this clause |
| 5, prepared | reserved for a loader's own prepared copy of an image --- its segments computed for another base, its state after its initialisers --- which a loader writes for itself and does not distribute; an image as distributed carries no note of this type, and a loader refuses one that does |
The first is required and the others are not. A count of zero states no
placement, and a loader reads a placement note only after the requirements note
states version 1.1 or higher. A loader compares the required interfaces with kal_interfaces and
refuses to load an image that requires one the implementation does not provide,
naming the interface. That is clause 6.1 in this arrangement: the loader binds
at load what a linker binds at link, so it reports at load what a linker reports
at link, and no operation that reports a lack of support at run time is
introduced. A loader refuses an image of a higher major version of this clause.
Whether a signature is required, and whose, is the loader's policy.
The loader, the format's tools, and the C library and C++ run-time inside an
image are not specified: they are programs above this interface, and an image may
carry any of them, or none. The reference loader and tools are published beside
this specification (mcpplibs/openkal-image) and are not normative.