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
97 changes: 55 additions & 42 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ jobs:
# 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
MCPP_VERSION: 2026.11.12.1
XLINGS_VERSION: v2026.10.26.1
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -202,53 +202,51 @@ jobs:
}
subst mcpp.toml .spec
subst examples/hello/mcpp.toml ../../.spec
# THE PLATFORM, AS THE SPECIFICATION IS: its repository's branch of the same name, else its
# default branch, named by path in the example (the example states the released version).
- name: Point at the platform's working tree
run: |
set -euo pipefail
git clone --quiet https://github.com/mcpplibs/openkal-uefi-platform .platform
if git -C .platform rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then
git -C .platform checkout --quiet "origin/$OPENKAL_BRANCH"
fi
f=examples/hello/mcpp.toml
sed "s|^platform = .*$|platform = { path = \"../../.platform\" }|" "$f" > "$f.next" && mv "$f.next" "$f"
grep -q 'path = "../../.platform"' "$f" || { echo "::error::$f was not substituted"; exit 1; }
- name: Install OVMF and qemu
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ovmf qemu-system-x86

- name: The example is a real EFI application
run: |
set -euo pipefail
cd examples/hello
mcpp build
EXE=$(find target -name "hello.exe" | head -1)
mcpp build --target x86_64-uefi
EFI=$(find target -name "hello.efi" | head -1)
RO=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-llvm/*/bin/llvm-readobj | sort -V | tail -1)
# Subsystem 10 (EFI application) is what firmware loads; no import table, since firmware
# satisfies no DLL.
"$RO" --file-headers "$EFI" | grep -q "IMAGE_SUBSYSTEM_EFI_APPLICATION"
if "$RO" --coff-imports "$EFI" | grep -qi "Name:"; then echo "::error::an import table"; exit 1; fi

# THE TOOLCHAIN THAT BUILT THE IMAGE, NOT LLVM.
#
# This target resolves to mingw-cross-gcc, so on a machine that has
# never built anything else there is no LLVM to borrow `llvm-readobj`
# from — the previous version of this step globbed for one and died on
# `ls: cannot access .../xim-x-llvm/*/bin/llvm-readobj`, AFTER the
# build had succeeded. The verification tool has to come from the same
# place as the compiler, or the check is only available on machines
# that happen to have another one.
OD=$(ls -d "$HOME"/.mcpp/registry/data/xpkgs/xim-x-mingw-cross-gcc/*/bin/x86_64-w64-mingw32-objdump | head -1)
test -x "$OD"

# Subsystem 10 is what makes firmware load it. A Windows console
# subsystem would build, link, and never be executed by UEFI.
# Measured spelling from this objdump: `Subsystem 0000000a (EFI application)`.
"$OD" -p "$EXE" | grep -qE 'Subsystem[[:space:]]+0000000a'

# And no import table: a DLL dependency cannot be satisfied by firmware.
if "$OD" -p "$EXE" | grep -qi "DLL Name"; then
"$OD" -p "$EXE" | grep -i -A2 "DLL Name"; echo "unexpected DLL import"; exit 1
fi

- name: It boots under OVMF and prints
# THE STATUS COMES BACK. The platform's board exit writes it to QEMU's debug-exit port, which
# ends the emulator with (status << 1) | 1: the example ends with 42 when every check held.
- name: It boots under OVMF, runs to its end and returns its status
run: |
set -euo pipefail
cd examples/hello
EXE=$(find target -name "hello.exe" | head -1)
mkdir -p esp/EFI/BOOT && cp "$EXE" esp/EFI/BOOT/BOOTX64.EFI
cp /usr/share/OVMF/OVMF_VARS_4M.fd vars.fd
timeout 90 qemu-system-x86_64 -machine q35 -m 256 -display none \
-serial file:uefi.log \
EFI=$(find target -name "hello.efi" | head -1)
mkdir -p esp/EFI/BOOT && cp "$EFI" esp/EFI/BOOT/BOOTX64.EFI
set +e
timeout 120 qemu-system-x86_64 -machine q35 -m 256 -nographic -no-reboot \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=pflash,format=raw,file=vars.fd \
-drive format=raw,file=fat:rw:esp -net none || true
cat uefi.log | tr -d "\\000" | head -40
grep -q "hello from openkal over UEFI" uefi.log
grep -q "pool ok" uefi.log
-drive if=virtio,format=raw,readonly=on,file=fat:esp \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 -net none < /dev/null > uefi.log 2>&1
status=$?
set -e
tr -d '\000' < uefi.log | tail -20
grep -q "all held" uefi.log
[ "$status" -eq 85 ] || { echo "::error::QEMU ended with $status, not 85 (42 << 1 | 1)"; exit 1; }

# ---------------------------------------------------------------------------
# The cross-compilation is performed FROM three systems, not only from Linux.
Expand Down Expand Up @@ -280,16 +278,16 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-14, windows-2022]
os: [macos-15, windows-2022]
defaults:
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
MCPP_VERSION: 2026.11.12.1
XLINGS_VERSION: v2026.10.26.1
XLINGS_NON_INTERACTIVE: '1'
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -477,13 +475,28 @@ jobs:
|| { echo "::error::$1 was not substituted"; exit 1; }
}
subst mcpp.toml .spec
subst examples/hello/mcpp.toml ../../.spec
# THE PLATFORM, AS THE SPECIFICATION IS: its repository's branch of the same name, else its
# default branch, named by path in the example (the example states the released version).
- name: Point at the platform's working tree
run: |
set -euo pipefail
git clone --quiet https://github.com/mcpplibs/openkal-uefi-platform .platform
if git -C .platform rev-parse --verify --quiet "origin/$OPENKAL_BRANCH" > /dev/null; then
git -C .platform checkout --quiet "origin/$OPENKAL_BRANCH"
fi
f=examples/hello/mcpp.toml
sed "s|^platform = .*$|platform = { path = \"../../.platform\" }|" "$f" > "$f.next" && mv "$f.next" "$f"
grep -q 'path = "../../.platform"' "$f" || { echo "::error::$f was not substituted"; exit 1; }
- name: The EFI application cross-builds
run: |
cd examples/hello
# TWICE, AND THE FIRST IS ALLOWED TO FAIL — every row of this
# matrix is a machine that has never targeted this triple, which is
# precisely where mcpp's lazy install of the target C library shows.
# The first build compiles sources needing its headers before they
# exist; the second has them. A single build here reported a
# portability failure that was really a cold-machine one.
mcpp build || true
mcpp build
mcpp build --target x86_64-uefi || true
mcpp build --target x86_64-uefi
test -n "$(find target -name hello.efi | head -1)"
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
target/
.mcpp/
compile_commands.json
mcpp.lock

# A working tree of the specification placed beside the sources. No trailing
compile_commands.json
.mcpp/
mcpp.lock
# slash: the pattern must match a symbolic link to one as well as a directory.
.spec
target/
110 changes: 40 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,92 +3,62 @@
An implementation of [openkal][kal] on UEFI Boot Services, for applications the
firmware loads before an operating system exists.

```toml
[build]
target = "x86_64-windows-gnu"
ldflags = ["-nostdlib", "-Wl,--subsystem,10", "-Wl,-e,efi_main"]
A program names the target and nothing else; the row `x86_64-uefi`, its compile
and link conventions and its runner are the platform package's
([openkal-uefi-platform][plat]), which selects this implementation:

[dependencies]
openkal = "0.14.1"
openkal-uefi = "0.8.1"
```toml
[target.x86_64-uefi]
platform = "openkal-uefi-platform@0.1.0"
```

## The target is `x86_64-windows-gnu`, and that is not a workaround

A UEFI application is PE/COFF with subsystem 10, entered through the Microsoft
x64 calling convention. Both are properties this toolchain already has, so
firmware function pointers are called directly rather than through per-call
annotations.

The first analysis of this backend concluded that mcpp would need a new "PE
freestanding" target before a UEFI application could exist at all. **Measured:
it does not.** The three link flags above produce
`IMAGE_SUBSYSTEM_EFI_APPLICATION (0xA)` with no DLL imports, which is what
firmware loads.
```sh
mcpp build --target x86_64-uefi # hello.efi: PE32+, subsystem EFI application
mcpp run --target x86_64-uefi # booted under QEMU with the edk2 firmware
```

## Why UEFI fits this specification
## What a program receives

openkal divides its interfaces by the **kind of resource** rather than by which
part of a standard library becomes available, and Boot Services divide the same
way. This backend is mostly forwarding:
The firmware calls `efi_main`; this implementation records its tables, turns off
the boot manager's watchdog, runs the program's static initialisers (read from
the image's own `.ctors` and `.CRT` sections, there being no C runtime) and calls
`main(argc, argv, envp)`. The arguments are the image's load options split at
spaces; firmware that gave none gives one empty name.

| openkal | UEFI |
|---|---|
| `stream` | `SIMPLE_TEXT_OUTPUT_PROTOCOL` |
| `memory` | `AllocatePool` / `FreePool` |
| `abort` | `Exit` |

`process` and `task` are absent because UEFI has no process model. An
application is the only thing running, and an interface provided in part would
be worse than one provided not at all — `import openkal.process;` does not
resolve, which is the honest answer rather than a set of calls that always fail.

The five interfaces version 0.8 adds are absent by the same rule. UEFI has no
process model to copy an address space within, so `space` is not provided; no
second context to bound a wait against, so `timeout` is not; and while Boot
Services do expose a network stack, this backend forwards only the protocols
listed above, so `net` and `datagram` are not. `terminal` is absent because
`SIMPLE_TEXT_OUTPUT_PROTOCOL` has no mode to read or set.
| `stream` | `SIMPLE_TEXT_OUTPUT_PROTOCOL` (UCS-2, `\n` as CR LF); input reports its end |
| `memory` | `AllocatePool` / `FreePool`, over-allocated for a stricter alignment |
| `env` | the load options; no variables, image, loader or system name (absent) |
| `time` | `GetTime` for the wall clock; the processor's counter, measured against `Stall`, for the monotonic one; `Stall` for a sleep |
| `abort` | `Exit` with the status as a warning code, so a starter reads the number |

Clause 6.1 makes each absence a link-time absence, so a program requiring one of
them is refused when it is built rather than when it runs.
`process` waits for `fs` over the firmware's file system protocol, since a
program is started by name through a directory; the specification's property for
an environment with one task (`KAL_PROCESS_PROP_RUNS_TO_END`, openkal 0.25.1
clause 7.19) is what it will claim. The rest have no firmware service beneath them.

## The three places this is not forwarding
## The board's exit

**Text.** openkal streams carry bytes; UEFI's console takes UCS-2 and treats a
bare line feed as a cursor movement that does not return the carriage. Every
write widens and translates, through a fixed-size buffer rather than an
allocation — a diagnostic path that allocates is one that can fail while
reporting a failure.

**Alignment.** `AllocatePool` has no alignment parameter and guarantees eight
bytes. A stricter request is satisfied by over-allocating and storing the
original pointer immediately before the aligned address, which is what a C
library does where the platform lacks `aligned_alloc`.

**Input.** `kal_stream_read` reports end of input rather than pretending.
UEFI's console input is a key-stroke protocol with a wait event, not a byte
stream; presenting it as one would give a reader something that appears to work
and silently loses every key that is not a plain character.
A status leaves an application through `Exit`, to whoever started it. A board on
which it must leave the machine --- an emulator running it as a test --- defines
`okuefi_board_exit(int)` in its platform package, and `kal_exit` calls it first;
the definition here does nothing. openkal-uefi-platform's writes the status to
QEMU's debug-exit port, only when the processor names QEMU as its hypervisor.

## The table offsets are checked, not trusted

These structures are ABI. A field written one slot out reads a neighbouring
function pointer — which calls something that exists, with the wrong arguments,
and the failure appears far from the cause. Every member this backend reaches is
pinned by a `static_assert` on its offset, so a mistake is a compile error.
These structures are ABI. A field one slot out reads a neighbouring function
pointer, which calls something that exists with the wrong arguments. Every member
this implementation reaches is pinned by a `static_assert` on its offset.

## Verified

`examples/hello` boots under OVMF in QEMU as `EFI/BOOT/BOOTX64.EFI`:

```
BdsDxe: starting Boot0001 "UEFI QEMU HARDDISK QM00001 " …
hello from openkal over UEFI
pool ok
```

`kal_exit` returns control to the firmware, which proceeds to its own boot
manager — the status reaches the loader rather than the program spinning.
`examples/hello`, built with mcpp 2026.11.12.1 and booted by dirun as
`EFI/BOOT/BOOTX64.EFI` under QEMU's q35 with the edk2 firmware (0.45 s, KVM):
a static initialiser ran, the arguments are counted, an over-aligned allocation,
a 20 ms sleep measured as such, the wall clock reads the present; status 42
returned through the board's exit.

[kal]: https://github.com/mcpplibs/openkal
[plat]: https://github.com/mcpplibs/openkal-uefi-platform
30 changes: 10 additions & 20 deletions examples/hello/mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
[package]
name = "hello"
version = "0.1.0"
version = "0.2.0"

[build]
target = "x86_64-windows-gnu"
# The three that make this an application firmware will load, rather than a
# Windows one. `-nostdlib` removes the C runtime and the default import
# libraries; `--subsystem,10` is EFI_APPLICATION; the entry point is the one
# UEFI calls, not `mainCRTStartup`.
ldflags = ["-nostdlib", "-Wl,--subsystem,10", "-Wl,-e,efi_main"]
[targets.hello]
kind = "bin"
main = "src/main.cpp"

[dependencies]
# THE FORM MUST MATCH THE ONE THE IMPLEMENTATION USES, not merely the
# version. mcpp refuses a graph in which one package reaches a dependency by
# version and another by git:
#
# dependency 'mcpplibs.openkal' is requested as both a version dep
# (by 'hello') and a git dep (by 'mcpplibs.openkal-uefi@path').
#
# 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.9.0"
openkal-uefi = { path = "../.." }
openkal = "^0.25.0"
# The implementation under change; the platform names openkal-uefi by version.
openkal-uefi = { path = "../..", override = true }

[target.x86_64-uefi]
platform = "openkal-uefi-platform@0.1.0"
38 changes: 27 additions & 11 deletions examples/hello/src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
// A UEFI application written against openkal, with no UEFI type in sight.
//
// `kal_main` and not `main`: nothing here supplies a C runtime, and the
// backend's `efi_main` is what firmware calls. That indirection is the point —
// the same source compiles against any openkal implementation.
// A UEFI application written against openkal, with no UEFI type in sight: a static initialiser,
// the arguments, the two clocks, an over-aligned allocation, and a status the board carries out.
import openkal.stream;
import openkal.abort;
import openkal.memory;
import openkal.env;
import openkal.time;

namespace {
void say(const char* s) {
kal_uintptr n = 0;
while (s[n]) ++n;
kal_stream_write(kal_stdout(), s, n);
}
int failures = 0;
void check(bool ok, const char* what) {
say(ok ? " ok " : " FAIL ");
say(what);
say("\n");
if (!ok) ++failures;
}
struct initialised {
int value = 7;
initialised() { value = 42; }
} g_initialised;
} // namespace

extern "C" [[noreturn]] void kal_main() {
int main(int argc, char**, char**) {
say("hello from openkal over UEFI\n");

void* p = kal_alloc(4096, 64); // over-aligned on purpose
say(p ? "pool ok\n" : "pool failed\n");
check(g_initialised.value == 42, "a static initialiser ran before main");
check(argc >= 1 && kal_env_arg_count() == static_cast<kal_uintptr>(argc), "the arguments are counted alike");
void* p = kal_alloc(4096, 64);
check(p != nullptr && (reinterpret_cast<kal_uintptr>(p) & 63) == 0, "an over-aligned allocation");
kal_free(p, 4096, 64);

kal_exit(p ? 0 : 1);
const kal_duration t0 = kal_time_monotonic();
kal_time_sleep(20u * 1000u * 1000u);
const kal_duration t1 = kal_time_monotonic();
check(t1 - t0 >= 19u * 1000u * 1000u && t1 - t0 < 2000u * 1000u * 1000u, "a sleep of 20 ms is measured as such");
check(kal_time_wall() > 1700000000ull * 1000000000ull, "the wall clock reads the present");
say(failures ? "FAILED\n" : "all held\n");
return failures ? 1 : 42;
}
Loading
Loading