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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,54 @@ jobs:
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
fi


# ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A
# CHANGE TO ONE.
#
# Another step in this file already clones the specification and
# substitutes it, and that made the arrangement look complete. It was not:
# the steps below reached `openkal' BY VERSION, so a version under review
# --- which by definition is not published --- failed them with
#
# E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the
# synced index ... the index is current, so this name is either
# wrong or not published yet
#
# ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across
# the eight repositories of this ecosystem while one change spanned all of
# them: seven jobs in three repositories had this shape, and every one of
# those repositories ALSO had a job that substituted correctly --- which is
# what made the gap invisible to a check done a repository at a time.
#
# These jobs are green on `main` and can only be green there, because
# there the published version is the one under test. That is the property
# that makes the defect silent: it is not a check that fails, it is a
# check that cannot run at the only time it would have something to say.
- name: Point at the specification's working tree
run: |
set -euo pipefail
git clone --quiet https://github.com/mcpplibs/openkal .spec
if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then
git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH"
echo "the specification is at $OPENKAL_BRANCH"
else
echo "the specification has no $OPENKAL_BRANCH; its default branch is used"
fi
# ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses
# a graph in which one package reaches a dependency by version and
# another by path, so substituting only the root leaves the build
# refusing for a second reason instead of the first.
# ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and
# BSD sed requires an argument to -i that GNU sed refuses. A temporary
# file is the spelling that holds on all three.
subst() { # subst <file> <relative-path-to-the-specification>
sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next"
mv "$1.next" "$1"
grep -q "path = \"$2\"" "$1" \
|| { echo "::error::$1 was not substituted"; exit 1; }
}
subst mcpp.toml .spec
subst examples/hello/mcpp.toml ../../.spec
- name: Install the emulator
run: |
# Both homes: the shim on PATH dispatches against whichever home owns
Expand Down Expand Up @@ -274,6 +322,9 @@ jobs:
run:
shell: bash
env:
# The specification is taken from the branch of the same name where one
# exists, so a change spanning both repositories is reviewed as a whole.
OPENKAL_BRANCH: ${{ github.head_ref || github.ref_name }}
MCPP_VERSION: 2026.8.27.1
XLINGS_VERSION: v2026.8.17.2
XLINGS_NON_INTERACTIVE: '1'
Expand Down Expand Up @@ -416,6 +467,53 @@ jobs:
echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)"
fi


# ⚠️⚠️ A JOB THAT RESOLVES THE PUBLISHED SPECIFICATION CANNOT REVIEW A
# CHANGE TO ONE.
#
# Another step in this file already clones the specification and
# substitutes it, and that made the arrangement look complete. It was not:
# the steps below reached `openkal' BY VERSION, so a version under review
# --- which by definition is not published --- failed them with
#
# E_NOT_FOUND: package 'compat.openkal@0.9.0' not found in the
# synced index ... the index is current, so this name is either
# wrong or not published yet
#
# ⭐⭐ THE UNIT IS THE JOB, NOT THE REPOSITORY. Measured 2026-08-28 across
# the eight repositories of this ecosystem while one change spanned all of
# them: seven jobs in three repositories had this shape, and every one of
# those repositories ALSO had a job that substituted correctly --- which is
# what made the gap invisible to a check done a repository at a time.
#
# These jobs are green on `main` and can only be green there, because
# there the published version is the one under test. That is the property
# that makes the defect silent: it is not a check that fails, it is a
# check that cannot run at the only time it would have something to say.
- name: Point at the specification's working tree
run: |
set -euo pipefail
git clone --quiet https://github.com/mcpplibs/openkal .spec
if git -C .spec rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then
git -C .spec checkout --quiet "origin/$OPENKAL_BRANCH"
echo "the specification is at $OPENKAL_BRANCH"
else
echo "the specification has no $OPENKAL_BRANCH; its default branch is used"
fi
# ⚠️ EVERY MANIFEST THAT NAMES IT, AND BY THE SAME FORM. mcpp refuses
# a graph in which one package reaches a dependency by version and
# another by path, so substituting only the root leaves the build
# refusing for a second reason instead of the first.
# ⚠️ NOT `sed -i'. This step runs on macOS and on Windows too, and
# BSD sed requires an argument to -i that GNU sed refuses. A temporary
# file is the spelling that holds on all three.
subst() { # subst <file> <relative-path-to-the-specification>
sed "s|^openkal = .*$|openkal = { path = \"$2\" }|" "$1" > "$1.next"
mv "$1.next" "$1"
grep -q "path = \"$2\"" "$1" \
|| { echo "::error::$1 was not substituted"; exit 1; }
}
subst mcpp.toml .spec
- name: The backend cross-builds
run: |
# ⚠️ TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ target/
.mcpp/
compile_commands.json
mcpp.lock

# A working tree of the specification placed beside the sources. No trailing
# slash: the pattern must match a symbolic link to one as well as a directory.
.spec
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ An implementation of [openkal][kal] on the RISC-V Supervisor Binary Interface.

```toml
[dependencies]
openkal = "0.5.1"
openkal-opensbi = "0.1.0"
openkal = "0.9.0"
openkal-opensbi = "0.3.0"
```

## ⭐ The portable RISC-V backend, as distinct from a board's own
Expand Down
2 changes: 1 addition & 1 deletion examples/hello/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
# That is the right refusal --- two forms can name two different trees --- and
# it means an example inside a repository follows that repository's own
# declaration while a change is in flight.
openkal = "0.8.0"
openkal = "0.9.0"
openkal-opensbi = { path = "../.." }
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[package]
namespace = "mcpplibs"
name = "openkal-opensbi"
version = "0.2.0"
version = "0.3.0"
description = "An implementation of openkal on the RISC-V Supervisor Binary Interface, portable across every machine whose firmware provides one"
license = "Apache-2.0"

Expand All @@ -33,7 +33,7 @@ repo = "https://github.com/mcpplibs/openkal-opensbi"
# The contract, not an implementation of it. Declaring it turns a version
# mismatch into a resolution-time message rather than a link-time one.
[dependencies]
openkal = "0.8.0"
openkal = "0.9.0"

# ⭐ WHAT RECEIVES CONTROL, WHICH IS A STATEMENT ABOUT THE PROGRAM.
#
Expand Down
26 changes: 14 additions & 12 deletions src/env.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,28 @@

extern "C" {

// A machine started by firmware receives no arguments and no named values, and
// the interface is provided rather than withheld: the answer "there are none"
// is an answer, and a program that asks is answered rather than failing to link.
//
// Each of these copies into the caller's buffer and reports the length the value
// has. Here there is no value, so each reports the condition --- which is what
// distinguishes "there is no such thing" from "there is one and it is empty",
// and is the distinction the interface exists to preserve.
kal_uintptr kal_env_arg_count(void) { return 0; }

const char* kal_env_arg(kal_uintptr, kal_uintptr* len) {
if (len) *len = 0;
return nullptr;
kal_intptr kal_env_arg(kal_uintptr, char*, kal_uintptr) {
return -kal_err_not_found;
}

kal_uintptr kal_env_var_count(void) { return 0; }

const char* kal_env_var(const char*, kal_uintptr, kal_uintptr* value_len) {
if (value_len) *value_len = 0;
return nullptr;
kal_intptr kal_env_var(const char*, kal_uintptr, char*, kal_uintptr) {
return -kal_err_not_found;
}

const char* kal_env_var_at(kal_uintptr, kal_uintptr* name_len,
const char** value, kal_uintptr* value_len) {
if (name_len) *name_len = 0;
if (value) *value = nullptr;
if (value_len) *value_len = 0;
return nullptr;
kal_intptr kal_env_var_at(kal_uintptr, char*, kal_uintptr) {
return -kal_err_not_found;
}

} // extern "C"
46 changes: 29 additions & 17 deletions src/kal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ bool dbcn_available() {
return g_dbcn == 1;
}

kal_io_result write_all(const unsigned char* p, kal_uintptr n) {
if (n == 0) return kal_io_result{0, kal_ok};
kal_intptr write_all(const unsigned char* p, kal_uintptr n) {
if (n == 0) return 0;

if (dbcn_available()) {
// The buffer is passed by physical address split into low and high
Expand All @@ -76,21 +76,24 @@ kal_io_result write_all(const unsigned char* p, kal_uintptr n) {
while (done < n) {
auto r = sbi_call(SBI_EXT_DBCN, SBI_DBCN_WRITE,
n - done, reinterpret_cast<kal_uintptr>(p + done), 0);
if (r.error != SBI_SUCCESS) return kal_io_result{done, kal_err_io};
if (r.error != SBI_SUCCESS)
return done ? static_cast<kal_intptr>(done) : -kal_err_io;
// A short write is legal here and is why the loop exists; openkal
// requires the whole buffer or a report, so the retry is the
// implementation's job rather than every caller's.
if (r.value <= 0) return kal_io_result{done, kal_err_io};
if (r.value <= 0)
return done ? static_cast<kal_intptr>(done) : -kal_err_io;
done += static_cast<kal_uintptr>(r.value);
}
return kal_io_result{done, kal_ok};
return static_cast<kal_intptr>(done);
}

for (kal_uintptr i = 0; i < n; ++i) {
auto r = sbi_call(SBI_EXT_LEGACY_PUTCHAR, 0, p[i], 0, 0);
if (r.error != SBI_SUCCESS) return kal_io_result{i, kal_err_io};
if (r.error != SBI_SUCCESS)
return i ? static_cast<kal_intptr>(i) : -kal_err_io;
}
return kal_io_result{n, kal_ok};
return static_cast<kal_intptr>(n);
}

// ── The heap ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -178,30 +181,29 @@ kal_stream kal_stdin (void) { return kal_stream{kStdin}; }
kal_stream kal_stdout(void) { return kal_stream{kStdout}; }
kal_stream kal_stderr(void) { return kal_stream{kStderr}; }

kal_io_result kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) {
if (s.h != kStdout && s.h != kStderr)
return kal_io_result{0, kal_err_invalid};
kal_intptr kal_stream_write(kal_stream s, const void* buf, kal_uintptr n) {
if (s.h != kStdout && s.h != kStderr) return -kal_err_invalid;
// ⚠️ Both streams reach the same console. SBI has one, and reporting two
// that are secretly one would be a claim the firmware cannot honour.
return write_all(static_cast<const unsigned char*>(buf), n);
}

kal_io_result kal_stream_read(kal_stream s, void* buf, kal_uintptr n) {
if (s.h != kStdin) return kal_io_result{0, kal_err_invalid};
if (n == 0) return kal_io_result{0, kal_ok};
kal_intptr kal_stream_read(kal_stream s, void* buf, kal_uintptr n) {
if (s.h != kStdin) return -kal_err_invalid;
if (n == 0) return 0;
auto* out = static_cast<unsigned char*>(buf);
if (dbcn_available()) {
auto r = sbi_call(SBI_EXT_DBCN, SBI_DBCN_READ,
n, reinterpret_cast<kal_uintptr>(out), 0);
if (r.error != SBI_SUCCESS) return kal_io_result{0, kal_err_io};
return kal_io_result{static_cast<kal_uintptr>(r.value), kal_ok};
if (r.error != SBI_SUCCESS) return -kal_err_io;
return static_cast<kal_intptr>(r.value);
}
auto r = sbi_call(SBI_EXT_LEGACY_GETCHAR, 0, 0, 0, 0);
// The legacy extension reports "nothing available" as a negative value,
// which is end of input as far as a reader is concerned.
if (r.error < 0) return kal_io_result{0, kal_ok};
if (r.error < 0) return 0;
out[0] = static_cast<unsigned char>(r.error);
return kal_io_result{1, kal_ok};
return 1;
}

// The firmware console is not buffered by this implementation, so there is
Expand All @@ -220,6 +222,16 @@ kal_uintptr kal_stream_props(kal_stream s) {
}

// ── openkal.memory ──────────────────────────────────────────────────────────
// The quantum this environment allocates and protects memory in.
//
// ONE, AND THAT IS AN ANSWER RATHER THAN AN ABSENCE. This machine has no
// memory management unit in this arrangement: memory is a fixed region and the
// allocator hands out any alignment a caller asks for, so every address and
// every length is acceptable. A caller that rounds to one is correct, which is
// what the operation promises. Reporting a page size the firmware does not
// enforce would be reporting a fact about some other machine.
kal_uintptr kal_memory_granularity(void) { return 1; }

void* kal_alloc(kal_uintptr size, kal_uintptr align) {
if (size == 0) size = 1;
if (align == 0) align = 1;
Expand Down
2 changes: 1 addition & 1 deletion src/time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,6 @@ void kal_time_sleep(kal_duration ns) {
while (ticks() < deadline) {}
}

const kal_uintptr kal_time_props = KAL_TIME_PROP_SLEEP_PRECISE;
kal_uintptr kal_time_props(void) { return KAL_TIME_PROP_SLEEP_PRECISE; }

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

// What this implementation says about itself before it is used.
//
// NOT AN INTERFACE, SO NOT CONDITIONAL ON ONE, AND NOT A BURDEN ON A MACHINE
// WITH NO OPERATING SYSTEM EITHER: both answers are constants. Clause 3.2 closes
// the set of core INTERFACES and these provide no resource; what they let a
// consumer do is ask which interfaces are here before it calls into one, which a
// consumer that is linked learns from the linker and one bound otherwise cannot.
extern "C" {

kal_u64 kal_version(void) { return KAL_VERSION; }

// ⚠️ THE WORD AGREES WITH WHAT IS EXPORTED, WHICH IS THE WHOLE OF ITS VALUE.
// This machine has no storage, no second image and no scheduler, so
// `openkal.fs', `openkal.process' and `openkal.task' are absent as definitions
// --- and the word says so rather than leaving a consumer to discover it by
// failing to link, which is the only report the earlier arrangement had.
kal_u64 kal_interfaces(void) {
return KAL_IFACE_ABORT | KAL_IFACE_STREAM | KAL_IFACE_MEMORY
| KAL_IFACE_ENV | KAL_IFACE_TIME;
}

}
Loading