Library Architecture
The ZBC host library is the code that lives on the host side – the
emulator, simulator, or developer machine that services semihosting
requests from a guest CPU. It exists in two implementations: a C90
reference library (Host API, Backend API,
Protocol API) and a C++17 alternative (C++ Host Library). Both
speak the identical RIFF wire protocol defined in RIFF-Based Semihosting Device Specification,
and the conformance test in test/conformance/test_conformance.cpp
enforces byte-for-byte equivalence between them on every CI run.
This page describes how the host library is structured – what the collaborators are, how they fit together, and where the customization seams are – in terms that apply equally to both implementations.
When to use which
Use the C host for C codebases, or where you want the zero-allocation, C90 implementation.
Use the C++ host for C++ emulators that want the
Backend/Policyobject model.
The collaborator model
Four collaborators, each independently replaceable:
GuestMemoryAbstract access to guest RAM (
readByte/writeByteand block helpers). The embedder implements it over the emulator’s memory model. In C this is a callback struct (zbc_host_mem_ops_t); in C++ it is thezbc::GuestMemorybase class.BackendPerforms the actual host I/O. A capability ladder:
Backend(inert) →ConsoleBackend(stdio, time, exit, timer) →FileBackend(files). Operations return a result value, an errno, and optional response data. In C this is thezbc_backend_tvtable; in C++ it is thezbc::Backendclass hierarchy returningzbc::OpResult.PolicyAuthorizes each operation, LSM-style:
allowOpen/allowWrite/resolvePath/ … are consulted before the backend runs. Secure by default (everything denied). Presets:ConsoleOnlyPolicy,SandboxedPolicy,UnrestrictedPolicy. Security is thus decoupled from capability – e.g. aFileBackendbehind aConsoleOnlyPolicycan only do console I/O.DeviceThe 32-byte memory-mapped peripheral. Owns the registers, decodes the 16-byte guest-native
RIFF_PTR, parses requests, runs the Policy-then-Backend dispatch, and writes responses back into the guest’s pre-allocated RETN/ERRO payloads. Conforms to spec v0.2.0: STATUS is a bitmask (TIMER / RESPONSE_READY / PROTO_ERROR), and unparseable requests are reported through theERROR_CODEregister without touching guest memory.
Platform configuration and CNFG
The host library is told the guest’s integer size, pointer size, and
byte order at construction time – via PlatformConfig in C++, via
zbc_host_set_platform_config() in C. Per the spec’s
“platform-provided defaults”, this makes the CNFG chunk an override
rather than a requirement: a request that omits CNFG is processed with
the platform configuration; a request that includes CNFG adopts its
values for the session. A host with platform-provided defaults never
raises Missing CNFG.
Error reporting
Both host libraries use the same tiered error model:
Normal results and host
errnogo in the RETN chunk.Protocol errors are written to the guest’s pre-allocated ERRO chunk when one exists and the container parsed.
If the request is unparseable (or no ERRO chunk was provided), the device writes nothing to guest memory and instead latches the code into the
ERROR_CODEregister and setsSTATUSbit 2 (PROTO_ERROR).
Integration sketch (C++)
The following sketch shows how the four collaborators come together in
the C++ host. The C library’s integration pattern – callback structs
plus the zbc_host_init / zbc_host_process entry points – is
covered in Emulator Integration.
#include "zbc/Semihost.h"
// 1. Bridge guest memory.
class MyMem : public zbc::GuestMemory {
public:
uint8_t readByte(uint64_t a) override { return Cpu.readByte(a); }
void writeByte(uint64_t a, uint8_t v) override { Cpu.writeByte(a, v); }
};
// 2. Construct the device with platform config, a backend, and a policy.
MyMem Mem;
zbc::PlatformConfig Cfg(/*int*/2, /*ptr*/2, zbc::Endian::Little); // 6502
auto OnExit = [](unsigned r, unsigned) { /* stop the machine */ };
auto OnTimer = [](unsigned hz) { return setTimerHz(hz); }; // false => EINVAL
zbc::Device Dev(Mem, Cfg,
std::make_unique<zbc::FileBackend>(OnExit, OnTimer),
std::make_unique<zbc::SandboxedPolicy>("/srv/sandbox"));
// 3. Wire the IRQ line for the periodic timer.
Dev.setIrqCallback([](bool assert) { cpu_set_irq(assert); });
// 4. Map the register window: forward CPU accesses in [base, base+32).
uint8_t on_read(uint64_t off) { return Dev.read(off); }
void on_write(uint64_t off, uint8_t v){ Dev.write(off, v); }
// 5. When your timer fires:
Dev.timerTick(); // sets STATUS.TIMER and asserts IRQ