Technical Brief 0005: The Device Resource Model ==================================================== *Technical Brief 0003* (:doc:`/briefs/tb0003-facet-based-capability-dispatch`) covers how a device or driver answers "do you support capability X". *Technical Brief 0004* (:doc:`/briefs/tb0004-device-tree-ownership-and-registries`) covers who owns a device and how anything finds it again. Neither addresses a third, more mundane question every real driver has to answer: **which I/O port, memory range, interrupt line, or DMA channel does this specific device instance actually use?** That is what a ``kapi::devices::resource`` answers. This brief covers its shape, why resources are assigned as data rather than hard-coded into a driver, and where that design does and does not protect a driver author from mistakes. The Problem This Replaces ------------------------------ A naive driver hard-codes its I/O addresses: .. code-block:: cpp // what a driver would look like without a resource model constexpr auto pit_port = std::uint16_t{0x40}; constexpr auto pit_irq = std::uint32_t{0}; This works exactly once, for exactly one bus topology, on exactly one machine — which is a description of most real hardware only by accident. The i8253 PIT lives at port ``0x40`` on every PC-compatible ISA bus, so the constant *happens* to be correct almost everywhere, but the fact that it is a compile-time constant baked into ``pit.cpp`` is still a latent bug: nothing about the PIT driver itself requires the ISA bus, that specific port range, or that specific IRQ. A second bus that also carries a PIT-compatible timer at a different address — or the same test running two PITs in a simulator for teaching purposes — has no way to tell the driver about it, short of editing the driver's source. The fix is to separate *what a device needs* (a driver-side concern — "some I/O ports and an IRQ") from *what a device has been given* (a bus-side concern — "these specific ports, this specific IRQ, discovered or configured however this bus discovers or configures things"), and to pass the second across a narrow, typed, runtime interface rather than a compile-time constant. That interface is ``kapi::devices::resource``. The Core Mechanism ----------------------- A resource is one of exactly four kinds, named by ``resource_type``: .. code-block:: cpp // kapi/kapi/devices/resource.hpp enum struct resource_type { mmio, // a range of the memory-mapped I/O address space port, // a range of I/O ports irq, // an interrupt line dma, // a direct memory access channel }; Each kind has its own small value type, tagged with its own ``resource_type`` as a ``static`` member: .. code-block:: cpp struct mmio_range { constexpr auto static type = resource_type::mmio; kapi::memory::physical_address start; kstd::bytes size; }; struct io_port { constexpr auto static type = resource_type::port; std::uint16_t start; std::size_t count; }; struct interrupt_line { constexpr auto static type = resource_type::irq; std::size_t number; }; struct dma_channel { constexpr auto static type = resource_type::dma; std::size_t number; }; ``resource`` itself is a tagged union over the four — a discriminant (``resource_type type``) alongside an anonymous ``union`` holding exactly one of the four value types at a time, with one converting constructor per kind: .. code-block:: cpp struct resource { explicit resource(mmio_range range); explicit resource(io_port port); explicit resource(interrupt_line irq); explicit resource(dma_channel channel); template [[nodiscard]] constexpr auto get() const { if (Type != this->type) { system::panic("[OS:DEV] Invalid resource access! {} != {}", Type, type); } // ... returns the matching alternative } resource_type type; private: union { mmio_range range; io_port port; interrupt_line irq; dma_channel channel; } m_value; }; A device carries a plain ``kstd::vector`` — an unordered bag, not a struct with four named slots — and exposes it through a narrow API on ``device`` (``kapi/kapi/devices/device.hpp``): .. code-block:: cpp //! Assign resources to this device. auto set_resources(kstd::vector resources) -> void; //! Get a specific resource assigned to this device. //! //! @param type The type of the resource to request. //! @param index The type-relative index of the resource to request. [[nodiscard]] auto request_resource(resource_type type, std::size_t index = 0) const -> kstd::result; //! Get a specific resource assigned to this device. template [[nodiscard]] auto request_resource(std::size_t index = 0) const { return request_resource(Type, index).transform([](auto resource) { return resource.template get(); }); } "Type-relative index" is precise and worth reading twice: ``index`` counts only among resources of the requested ``type``, not among all of a device's resources. A device with two I/O port ranges and one IRQ line addresses its second port range as ``request_resource(1)`` and its (only) IRQ as ``request_resource(0)`` — the port ranges and the IRQ never compete for the same index space, regardless of the order they were assigned in. Assignment happens on the bus side, before the device is even attached to the tree, and consumption happens on the driver side, inside ``probe()``. The complete real example is the ISA PIT (``arch/x86_64/arch/devices/init.cpp``): .. code-block:: cpp auto pit_device = kstd::make_shared(); pit_device->set_resources({ kapi::devices::resource{kapi::devices::io_port{.start = 0x40, .count = 4}}, kapi::devices::resource{kapi::devices::interrupt_line{.number = 0}}, }); isa_bus->add_child(pit_device); and the driver that later requests them back (``arch/x86_64/arch/drivers/pit.cpp``): .. code-block:: cpp auto pit::probe(kapi::devices::device & device) -> kstd::result { auto port = device.request_resource(); if (!port) { return kstd::failure(port.error()); } auto irq = device.request_resource(); if (!irq) { return kstd::failure(irq.error()); } auto base_port = port->start; auto irq_number = irq->number; // ... programs the hardware using base_port and irq_number, never a literal return kstd::success(); } Nothing in ``pit.cpp`` mentions ``0x40`` or ``0``. The driver asks for "my port range" and "my IRQ", by type, and gets back whatever the bus that actually discovered or configured this specific device instance decided to assign. The same driver binary would work unmodified against a PIT wired to a different port on a different (hypothetical) bus. Why a Panicking ``get()``, Not a Recoverable One ----------------------------------------------------------- ``resource::get()`` calls ``system::panic()`` if the stored discriminant does not match the requested ``Type`` — a hard stop, not an error code. This is a deliberate asymmetry with facet dispatch (:doc:`/briefs/tb0003-facet-based-capability-dispatch`), where an unsupported capability is a routine, expected, recoverable ``nullptr``. The two situations are not the same kind of "wrong": whether a device *has* a given facet is discovered dynamically and is often expected to be false (most devices do not support most facets) — a recoverable result is the only sane API. Whether a caller who already holds a ``resource`` object of a *known, compile-time* ``resource_type`` asks ``get()`` for the *matching* ``Type`` is not a discovery at all — the caller wrote the mismatched ``Type`` themselves, at the same call site, in the same function. Reaching that panic requires a programmer to type ``resource{io_port{...}}.get()`` or the moral equivalent — a bug in the caller's own code, not a fact about the hardware. ``request_resource(type, index)`` is where the genuinely recoverable case lives instead: "does this device have a port resource at all" is answered by ``kstd::result``, exactly like an unsupported facet, and the templated convenience overload only reaches ``get()`` after that recoverable check has already succeeded — so ``get()``'s ``Type`` and the request's ``Type`` are, by construction, always the same value on that path. Two Real Fixes, Two Real Lessons -------------------------------------- The resource model's git history is short but instructive — two consecutive fix commits, both from the same afternoon, both worth reading directly rather than summarizing away. The type initially shipped with a fifth, sentinel ``resource_type::invalid`` and a matching ``invalid_resource`` union member, and ``resource::type`` defaulted to it (``resource_type type{resource_type::invalid};``). A later commit (``1121cb81``, "kapi: fix device resources") removed both entirely, and changed ``get()`` from silently falling through the ``if constexpr`` chain on a mismatch to explicitly panicking. The sentinel added a fifth state that every ``switch`` over ``resource_type`` had to account for, for a case that should have been structurally impossible — every ``resource`` is constructed through one of the four typed, converting constructors, so there was never a code path that legitimately produced an ``invalid`` resource; the sentinel exists only to be defended against, not to be reached. Removing it is a small, telling example of a broader principle: a "just in case" default state that the type's own constructors can never actually produce is not defensive programming, it is an extra case for every future ``switch`` to handle correctly, forever, in exchange for catching nothing real. The other fix (``dca83530``, "kapi: fix device resource lookup", landing slightly earlier the same day) corrected ``request_resource``'s actual lookup logic. The original implementation enumerated the device's *entire* resource vector first and then filtered by type: .. code-block:: cpp // before dca83530 — index counted across ALL resources, not just matching ones auto numbered_resources = std::views::enumerate(m_resources); auto found = std::ranges::find_if(numbered_resources, [&](auto entry) { auto const & [number, resource] = entry; return resource.type == type && static_cast(number) == index; }); against the corrected version, which filters first and enumerates the *filtered* view: .. code-block:: cpp // after dca83530 — index counts only among resources of the requested type auto filtered = std::views::filter(m_resources, [&](auto e) { return e.type == type; }); auto numbered_resources = std::views::enumerate(filtered); auto found = std::ranges::find_if( numbered_resources, [&](auto entry) { return static_cast(std::get<0>(entry)) == index; }); The difference is exactly the "type-relative index" the header comment already promised: with the original ordering, a device with one port range followed by one IRQ could never successfully look up that IRQ as index ``0`` (it sits at position ``1`` in the unfiltered vector), silently contradicting its own documentation. The fix is a two-line reordering of a ``std::ranges`` pipeline, and a good illustration of how easy it is for a filter-then-number operation to be written number-then-filter by mistake — the types all still line up, the code still compiles, and the bug only shows itself as a request that should succeed returning "not found" for specific, order-dependent combinations of resource assignments. Benefits ----------- * **A driver never hard-codes an address that belongs to the bus.** The PIT driver above has no ISA-specific or PC-specific constant anywhere in it; every address comes from ``request_resource()``, sourced from whatever assigned it. * **The tagged union has no wasted state.** Since the ``invalid`` sentinel was removed, every ``resource`` that exists represents a real, fully-formed value of one of the four kinds — there is no "resource in an unset state" to defend against at every use site. * **Type-relative indexing composes cleanly with multiplicity.** A device with several resources of the same kind (two MMIO ranges, three DMA channels) addresses each one independently of how many resources of *other* kinds it also happens to carry, and independently of assignment order. * **The recoverable/panicking split matches where the actual risk is.** "Does this device have a port resource" is a real question about hardware and gets a real, checkable answer; "did I ask a ``resource`` I already extracted for the wrong compile-time type" is a caller bug and gets a panic that points straight at the mistake. Drawbacks and Limitations ------------------------------ Stated as plainly as the benefits above. * **No overlap or conflict detection, anywhere.** ``set_resources()`` stores whatever ``kstd::vector`` it is given, without checking it against any other device's resources — two devices can be assigned overlapping I/O port ranges, or the same IRQ number, and nothing in the resource model itself will notice, let alone refuse. Linux's ``struct resource`` tree (``request_region()``/``request_mem_region()``) does exactly this kind of conflict detection as its central job — a real point of contrast, not just a missing nice-to-have, since it is the feature that lets Linux tell a user *which* two drivers are fighting over the same port range instead of silently corrupting whichever one loses the race. * **No automatic release on unbind.** A resource assigned via ``set_resources()`` stays assigned for the device's lifetime; there is no bus-side bookkeeping that reclaims a port range or IRQ when a driver unbinds, and no equivalent of ``release_region()``. Reassigning a resource elsewhere after a device is torn down is a manual, out-of-band exercise for whatever code manages that bus. * **The bag-of-resources shape has no schema.** A ``kstd::vector`` says nothing about how many resources of which kinds a given driver actually expects; ``request_resource(2)`` failing because the device only has two port ranges (indices 0 and 1) looks, from the driver's perspective, identical to a bus author who simply forgot to assign the third one. Nothing catches this earlier than the same ``probe()`` failure a genuine hardware absence would produce. * **``request_resource()`` returns a snapshot copy, not a live view.** Because ``resource`` is a small value type returned by value inside a ``kstd::result``, nothing prevents ``set_resources()`` from being called again on an already-bound device out from under a driver that cached an earlier ``request_resource()`` result — the resource model has no notion of "resources are fixed once a driver is bound" to enforce. * **The panic in ``get()`` is only as good as the caller already knowing ``Type``.** It protects against a literal typo at a ``get<>()`` call site; it does nothing for a driver that requests the *right* type but the *wrong* index, or one that never checks whether ``request_resource()`` returned an error at all before dereferencing the result. Parallels in Other Systems ------------------------------- **ACPI's** ``_CRS`` (Current Resource Settings) object is the closest firmware-level analogue: a per-device, byte-encoded list of the same four broad categories — memory ranges, I/O port ranges, interrupts, and DMA channels — that the ACPI-aware OS parses and hands to the matching driver, discovered and assigned by firmware rather than hard-coded anywhere in the OS [1]_. The shape (an unordered bag of typed, per-device resource descriptors, consumed by whatever driver later binds) is essentially the same idea TeachOS's ``resource`` vector implements in miniature. **Devicetree's** ``reg``, ``interrupts``, and (on platforms that use it) ``dma-channels`` properties play the identical role for firmware-described, non-discoverable buses common in embedded systems: a node in the device tree lists the address ranges and interrupt lines it owns, and the OS's platform bus code walks the tree assigning them to matching drivers, the same enumerate-then-assign shape as ``init_legacy_devices()`` walking the ISA bus above [2]_. **Linux's** ``struct resource`` is the mechanism this brief has already contrasted directly: a tree (not merely a list) of address ranges, with ``request_region()``/``request_mem_region()`` performing exactly the overlap detection TeachOS's model omits, returning failure if the requested range intersects one already claimed [3]_. Reading Linux's resource code after this brief is a natural next step precisely because the shape is so recognizable and the one place it diverges — a global, conflict-checked tree of *all* address space, not just per-device bags — is worth understanding as a deliberate simplification here, not an oversight. **FreeBSD's** ``rman`` (resource manager) generalizes the same idea one step further: an ``rman`` instance owns a numeric range (I/O ports, memory, IRQs) and hands out sub-ranges via ``rman_reserve_resource()``, tracking which sub-ranges are currently allocated so a second reservation over an already-claimed range fails — the same conflict-detection property Linux's tree gives, implemented as an explicit, reusable allocator rather than a single global tree [4]_. References ------------- .. [1] UEFI Forum, Inc., *Advanced Configuration and Power Interface (ACPI) Specification*, section on "Device Configuration" and the ``_CRS``/``_PRS``/``_SRS`` control methods. `Online `_. .. [2] devicetree.org, *Devicetree Specification*, sections on the ``reg`` and ``interrupts`` properties. `Online `_. .. [3] Corbet, J., Rubini, A., and Kroah-Hartman, G., *Linux Device Drivers*, 3rd ed., O'Reilly, 2005, chapter 9, "Communicating with Hardware" (I/O port and memory region allocation, ``request_region()``). See also the current kernel source, ``kernel/resource.c`` and ``include/linux/ioport.h``. .. [4] McKusick, M. K., Neville-Neil, G. V., and Watson, R. N. M., *The Design and Implementation of the FreeBSD Operating System*, 2nd ed., Addison-Wesley, 2014, chapter on bus and device management (``newbus``, ``rman(9)``). See also the FreeBSD ``rman(9)`` manual page. `Online `_. .. seealso:: ``kapi/kapi/devices/resource.hpp`` and ``kernel/kapi/devices/device.cpp`` (``request_resource()``/``set_resources()``) — the mechanism this brief quotes from directly. ``arch/x86_64/arch/devices/init.cpp`` and ``arch/x86_64/arch/drivers/pit.{hpp,cpp}`` — the complete assignment-and-consumption example this brief walks through. :doc:`tb0003-facet-based-capability-dispatch` — the contrasting design for a genuinely dynamic, recoverable capability query, and why resources deliberately do not follow it. :doc:`tb0004-device-tree-ownership-and-registries` — resources live on the same ``device`` object whose ownership and locking that brief covers; ``device::request_resource()`` takes the same ``tracked_mutex`` guard described there. :doc:`../guides/device-drivers` — resources in practical use, inside a real ``probe()``.