An implementation of openkal on the RISC-V Supervisor Binary Interface.
[dependencies]
openkal = "0.5.1"
openkal-opensbi = "0.1.0"A board-supplied backend writes to a device address, and that address is a board fact. The same binary on a second RISC-V machine writes to something that is not a UART: it compiles, links, runs, and prints nothing.
SBI has no such property. The console here is a call into firmware that already
knows the machine, so one image runs under OpenSBI on QEMU's virt and on a real
board without being rebuilt.
Both kinds are legitimate, and a project chooses by which property it needs: SBI requires firmware beneath it, a board backend does not.
abort, stream and memory — openkal's core set — plus time and env. An
implementation provides an interface in whole or not at all, so the absence of
fs, process and task is not a deviation; import openkal.task; simply
does not resolve. This machine has no storage, no second image to start and no
scheduler, and clause 6.2 says the remedy for an operation that cannot be
provided is that its absence be expressed by its absence rather than by a
run-time refusal.
time used to be on that list, with a reason, and the reason was wrong.
It read: SBI can arm a timer interrupt, which is a mechanism for a kernel rather
than a clock a program can read. The first half is true. The second does not
follow from it — this architecture also exposes rdtime directly to the
program, through the time CSR, independently of SBI's timer extension. Two
different facilities, and the absence of the first says nothing about the
second.
Measured 2026-08-23 under OpenSBI on QEMU's virt, from supervisor mode with no
kernel beneath:
t0=333572 t1=381292 ADVANCES
⭐ The conclusion got rechecked and the reason beside it did not. The two minutes that refuted it had been available for as long as the file existed.
time is therefore provided: a monotonic count, an exact granularity, and a
sleep that spins on the same counter — which on a machine with one execution
context and nothing to yield to is what sleeping is, so
KAL_TIME_PROP_SLEEP_PRECISE is set truthfully. Not a wall clock: SBI
defines no facility for one, KAL_TIME_PROP_WALL_AVAILABLE is clear, and clause
6.2 makes that a property rather than a missing interface.
env is provided and every answer is empty. That is an implementation, not a
stub: firmware enters the image with a hart identifier and a device tree, and
neither is a command line, so this environment has an environment and it has
nothing in it. A caller enumerating zero things needs no special case, which is
exactly what distinguishes this from the run-time refusal clause 6.2 forbids.
SBI provides no allocator, so this one hands out from a static region and
kal_free does nothing. openkal's criterion admits it: an implementation that
can be exhausted bounds what is available, whereas one that makes its callers
silently wrong is a simulation. Exhaustion is a defined outcome — kal_alloc
returns null — and every caller already handles it.
The region is 64 KiB by default. A project that needs another figure overrides
OPENKAL_OPENSBI_HEAP_BYTES rather than editing this package. A program whose
allocation pattern needs reuse should place a real allocator above this one;
that is policy, and openkal carries mechanism.
board.ld states where the firmware hands control over (0x80200000, since
OpenSBI itself occupies the start of RAM), what the layout has to contain for a
C++ program to work — the initialiser arrays, the unwind tables and their bounds,
the thread-local segment — and how much stack and heap the image reserves. It
reaches a consumer's link line through this package's build program, in the same
way its kal_* definitions reach the consumer's objects.
A program therefore states nothing. Until 2026-08-23 every program that wanted to
run here carried a copy: examples/hello had one, the C++ runtime's
same-source example had a second, and the specification's conformance suite
would have needed a third. None of those is a property of a program.
ld.lld: error: section .eh_frame file range overlaps with .debug_str_offsets
The sizes — 256 KiB of stack, 16 MiB of heap — are generous rather than minimal,
and that is the one judgement in the file. A C program printing a string needs
neither; a program carrying a C library and a C++ standard library does, and it
exhausts smaller figures during its own initialisation, before main, so what it
looks like is a program that starts and prints nothing. Both regions lie past the
end of the image and cost nothing in it. A board with far less memory states its
own map instead of taking this one.
DBCN is probed, not assumed. The Debug Console extension arrived in SBI
v2.0, and older firmware answers SBI_ERR_NOT_SUPPORTED — on which every write
would silently transfer nothing. The legacy one-character extension is the
fallback.
static. A guarded local static compiles to
__cxa_guard_acquire/__cxa_guard_release, which a freestanding target has no
runtime to supply. Measured: the link fails naming both. -fno-threadsafe-statics
would also silence it, but a flag that has to be remembered is weaker than a
construct that cannot require the runtime in the first place.
examples/hello runs under QEMU with -bios default, which is real OpenSBI:
hello from openkal over SBI
heap ok
The image carries no C library ([target.<triple>].sysroot = "") and no board
package, and it states no memory map of its own: board.ld above is this
package's.
The specification's conformance suite also runs here, under the same firmware,
selected to the core set — twelve observations held, none failed. Clause 9
makes the behavioural half a property of every implementation, and an
implementation of a machine with no operating system is not exempt from it.