diff options
Diffstat (limited to 'docs/briefs')
| -rw-r--r-- | docs/briefs/tb0003-facet-based-capability-dispatch.rst | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/docs/briefs/tb0003-facet-based-capability-dispatch.rst b/docs/briefs/tb0003-facet-based-capability-dispatch.rst new file mode 100644 index 00000000..7027a700 --- /dev/null +++ b/docs/briefs/tb0003-facet-based-capability-dispatch.rst @@ -0,0 +1,430 @@ +Technical Brief 0003: Facet-Based Capability Dispatch in the Device Model +========================================================================== + +TeachOS is built as a freestanding C++23 kernel, compiled with ``-fno-rtti`` and without exceptions. +This rules out the two mechanisms most C++ programmers reach for when a piece of code needs to ask an object "can you do X, and if so, give me the part of you that does it": ``dynamic_cast`` and ``typeid``. +Both depend on RTTI (Run-Time Type Information), a compiler-maintained metadata structure that a freestanding kernel does not, and should not, pay for [1]_. +Yet the device model needs exactly this kind of question answered constantly: *does this device expose block storage? does this driver claim to handle ISA devices named "pit"? is this node in the tree itself a bus, so that removing it must recurse into its children?* + +TeachOS answers all of these with a single, small mechanism called a **facet**. +This brief documents its design rationale, how to use it correctly, where it is used in the current tree, how it compares to capability-query mechanisms in other systems, and — honestly — where it falls short. + +.. note:: + + An earlier iteration of this mechanism used the name ``interface``/``interface_id``/``query_interface()`` throughout the tree. + A single sweeping rename (``chore: replace "interface" with "facet"``) is recorded in the repository's history and replaced every occurrence with ``facet``/``facet_id``/``query_facet()`` — the name used throughout the current source tree and in this brief. + A reader tracing old commits or reviewing older diffs should expect to see the earlier name; it refers to exactly the same mechanism documented here. + +The Problem This Replaces +-------------------------- + +Before this mechanism existed, TeachOS's device layer answered capability questions with a growing set of virtual booleans directly on the base ``device`` class — the pattern ``virtual auto is_block_device() const -> bool``, with a `static_cast` following wherever the answer was ``true``. +This has three compounding problems, all observed directly in the pre-facet tree: + +* **The base class must know every future device class.** Adding character devices, network devices, or input devices means adding ``is_char_device()``, ``is_net_device()``, ``is_input_device()`` to ``device`` itself — the most fundamental, most widely-included type in the system grows with every new *kind* of device anyone ever adds, forever. +* **The cast is unchecked.** ``is_block_device()`` returning ``true`` is a promise, not a guarantee. Nothing stops a type from overriding the bool without actually inheriting the corresponding type; the following ``static_cast`` is then undefined behavior. +* **Failure has nowhere honest to go.** With no capability check available beyond a bool, a mismatch (e.g. calling ``read()`` on a device that turns out not to be block-capable) was reachable only by trusting the bool and panicking if it lied — including from a path userspace can trigger through ``open()``/``read()``. + +Facet dispatch closes all three: capability tags are defined next to the capability itself, in whatever module owns it, with zero changes to ``device``; a facet query returns ``nullptr`` rather than performing an unchecked cast on your behalf; and a missing facet is a normal, checkable condition, not a promise that was broken. + +The Core Mechanism: One Primitive, Several Call Sites +------------------------------------------------------- + +The entire design rests on one idea, repeated at four points in the device model. Understanding the primitive once means the other three sites need no new concepts, only new context. + +The Facet Tag +~~~~~~~~~~~~~~ + +A facet is identified by a ``kapi::capabilities::facet_id`` — a small, ``constexpr``-constructible value wrapping a ``std::string_view``, compared by value: + +.. code-block:: cpp + + struct facet_id + { + constexpr explicit facet_id(std::string_view name) : m_name{name} {} + [[nodiscard]] constexpr auto name() const noexcept -> std::string_view { return m_name; } + constexpr auto operator==(facet_id const &) const noexcept -> bool = default; + + private: + std::string_view m_name; + }; + +Every facet type — a pure abstract interface, in the ordinary C++ sense — declares one of these as a ``static constexpr`` member named ``id``, conventionally right next to its own declaration: + +.. code-block:: cpp + + struct block_special_file + { + constexpr auto static id = kapi::capabilities::facet_id{"block"}; + + virtual ~block_special_file() = default; + [[nodiscard]] virtual auto read_block(std::size_t block_index, std::span<std::byte> buffer) + -> kstd::result<kstd::bytes> = 0; + // ... + }; + +Nothing about ``facet_id`` or the facet types themselves lives inside ``kapi::devices``. +``block_special_file`` is defined in ``kapi::filesystem``; ``isa_signature``/``isa_claim`` (below) are defined in ``arch::bus``. +The device layer knows the tag mechanism, never the tags. + +Querying a Single Object +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``kapi::devices::device`` (and, symmetrically, ``driver``) exposes exactly one customization point, ``query_facet()``, and builds three convenience wrappers on top of it: + +.. code-block:: cpp + + struct device : kstd::enable_shared_from_this<device> + { + [[nodiscard]] auto facet(kapi::capabilities::facet_id id) noexcept -> void *; + + template<typename FacetType> + [[nodiscard]] auto facet() -> FacetType * + { + return static_cast<FacetType *>(facet(FacetType::id)); + } + + template<typename FacetType> + [[nodiscard]] auto has_facet() const noexcept -> bool + { + return has_facet(FacetType::id); + } + + protected: + auto virtual query_facet(kapi::capabilities::facet_id facet) -> void *; + }; + +The base implementation of ``query_facet()`` simply returns ``nullptr``. +A concrete device overrides it, checks the requested id against every facet it supports, and — this is the part every override must get right — falls through to the base class's ``query_facet()`` for everything else, so the chain of facets contributed by every base class stays reachable: + +.. code-block:: cpp + + // arch/x86_64/arch/devices/pit.hpp + struct pit final : kapi::devices::device, arch::bus::isa_signature + { + pit(); + [[nodiscard]] auto isa_name() const -> std::string_view override; + + protected: + auto query_facet(kapi::capabilities::facet_id facet) -> void * override; + }; + + // arch/x86_64/arch/devices/pit.cpp + auto pit::query_facet(kapi::capabilities::facet_id facet) -> void * + { + if (facet == arch::bus::isa_signature::id) + { + return static_cast<arch::bus::isa_signature *>(this); + } + return kapi::devices::device::query_facet(facet); // <-- the delegation + } + +The ``static_cast`` here is safe *by construction*, not by promise: it only ever fires when the object actually multiply-inherits from ``isa_signature``, because that is the only way ``this`` could correctly convert to ``isa_signature *``. +The single ``static_cast`` per facet is confined to exactly this line, in exactly this file — nowhere else in the codebase casts a ``device *`` based on a capability bool. + +Symmetric Queries on Drivers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``driver`` carries the identical shape — ``facet<T>()``/``has_facet<T>()``/``query_facet()`` — for a reason that matters once matching enters the picture: identification is not a property of devices alone. +A driver has to be able to say *what it claims to support* using the same vocabulary a device uses to say *what it is*: + +.. code-block:: cpp + + // arch/x86_64/arch/bus/isa.hpp + struct isa_claim + { + constexpr auto static id = kapi::capabilities::facet_id{"isa_claim"}; + virtual ~isa_claim() = default; + [[nodiscard]] virtual auto supported_names() const -> std::span<std::string_view const> = 0; + }; + +``isa_signature`` (device-side, what a device *is*) and ``isa_claim`` (driver-side, what a driver *claims*) are a matched pair, both defined by the ISA bus, both dispatched through the exact same ``facet<T>()`` primitive. +This ``*_signature``/``*_claim`` naming is the project convention for every such pair — a bus that wants to identify its devices defines one facet with each suffix and nothing else. + +Facets in Bus Matching +~~~~~~~~~~~~~~~~~~~~~~~~ + +``bus_protocol::match()`` is where the two queries above meet: + +.. code-block:: cpp + + // arch/x86_64/arch/bus/isa.cpp + [[nodiscard]] auto match(kapi::devices::device const & device, kapi::devices::driver const & driver) const + -> kstd::result<std::uint32_t> override + { + auto device_signature = device.facet<isa_signature>(); + auto driver_claim = driver.facet<isa_claim>(); + + if (!device_signature || !driver_claim) + { + return kstd::failure(kapi::devices::driver_match_errc::no_match); + } + if (!std::ranges::contains(driver_claim->supported_names(), device_signature->isa_name())) + { + return kstd::failure(kapi::devices::driver_match_errc::no_match); + } + return 0u; // ISA has no notion of a "better" match; every hit is equally certain. + } + +``driver_registry::try_bind()`` calls exactly this ``match()`` for every registered driver against a candidate device, keeps every driver that returns a priority, and binds the highest one — falling through to the next-best candidate if the winner's own ``probe()`` subsequently fails. +Neither ``driver_registry`` nor ``bus_protocol`` needs to know that ISA identification is a bare string; a future PCI ``bus_protocol`` matches on a ``{vendor, device}`` facet pair instead, with zero changes anywhere in this chain. + +A device is not only queried for identification facets. ``bus`` itself answers the question "are you a bus" as a facet — including on itself: + +.. code-block:: cpp + + // kernel/kapi/devices/bus.cpp + auto bus::query_facet(kapi::capabilities::facet_id facet) -> void * + { + if (facet == bus::id) { return this; } + else if (facet == bus_protocol::id) { return m_protocol; } + return device::query_facet(facet); + } + + // used elsewhere in the same file, to recurse correctly during teardown: + if (auto child_bus = device.facet<bus>()) + { + // this child is itself a bus; its own children must be torn down first + } + +This is worth pausing on: facet dispatch is not only used for "does this device support a POSIX file capability" style questions. It is the *general* answer to "what can I do with this device", including purely structural facts about the tree itself. ``bus_protocol`` is even published as a facet of the bus that owns it (``m_protocol``, set once at construction and handed back as-is) — the composition case introduced properly in the next section. + +The Facet Registry: An Index Across the Whole Tree +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Everything above answers "does *this specific* device or driver support facet X" for an object you already hold a reference to. A different, equally common question is "which devices, anywhere in the tree, currently support facet X" — the question devfs asks to build ``/dev``, or the question ``ram_disk``'s driver asks to number its own instances. This is ``kapi::devices::facet_registry``: a single, generic, system-global index, keyed by ``facet_id``, holding only non-owning references: + +.. code-block:: cpp + + struct facet_registry + { + auto static get() -> facet_registry &; + + template<typename FacetType> + auto publish(kstd::shared_ptr<device> device, kstd::string name) -> kstd::result<void>; + + template<typename FacetType> + auto publish(kstd::shared_ptr<device> device, kstd::string name, FacetType * facet) -> kstd::result<void>; + + auto withdraw(device const & device, kapi::capabilities::facet_id id) -> void; + + [[nodiscard]] auto all(kapi::capabilities::facet_id id) const -> kstd::vector<entry>; + [[nodiscard]] auto resolve(kapi::capabilities::facet_id id, std::string_view name) -> void *; + + auto subscribe(kstd::weak_ptr<facet_registry_observer> observer) -> void; + // ... + }; + +There is deliberately **one** registry type, not one per capability. It knows nothing about "block" or "char" or any other specific facet — ``publish<FacetType>()``/``all()``/``resolve()`` are generic over the facet type, so adding a wholly new kind of capability (a future network device, say) needs no new registry, no new bookkeeping module, and no change to this type at all. Because entries are held by ``weak_ptr``, a device that dies without being explicitly withdrawn simply stops appearing in ``all()`` and ``resolve()`` results, rather than leaving a dangling handle for a caller to trip over. + +Two Ways to Implement a Facet +-------------------------------- + +The two ``publish()`` overloads above correspond to two genuinely different ways a facet gets implemented, and mixing them up is the single most common point of confusion for a newcomer, so it is worth making explicit. + +**Inheritance** — the device type itself is the facet. This is the ``pit``/``isa_signature`` case above: ``pit`` multiply-inherits ``isa_signature``, its own ``query_facet()`` hands back ``this``, and both ``device.facet<isa_signature>()`` *and* ``kapi::devices::publish_facet<isa_signature>(device, name)`` reach the same object. Use this when the device concretely *is* the thing being asked about, and the type is small and dedicated to one purpose. + +**Composition** — a separate object, owned by the driver rather than the device, implements the facet, and is published via the second ``publish()`` overload that takes an explicit implementation pointer. ``ram_disk``'s driver does exactly this: + +.. code-block:: cpp + + // kernel/kernel/drivers/storage/ram_disk.cpp + struct block_node final : kapi::filesystem::block_special_file + { + explicit block_node(kapi::boot_modules::module const & module) : m_module(module) {} + // read_block / write_block / block_size / capacity ... + private: + kapi::boot_modules::module m_module; + }; + + auto ram_disk::probe(kapi::devices::device & device) -> kstd::result<void> + { + // ... + auto implementation = kstd::make_shared<struct block_node>(module); + auto published = kapi::devices::publish_facet<kapi::filesystem::block_special_file>( + device.shared_from_this(), name, implementation.get()); + // ... + device.set_driver_data(implementation); // keeps block_node alive for as long as the binding lasts + return kstd::success(); + } + +Here, ``device`` (the plain ``kapi::devices::device`` node returned by boot-module enumeration) never overrides ``query_facet()`` for ``block_special_file`` at all — it has no idea the facet exists. **The registry, not the device, is what makes this facet discoverable**: ``facet_registry::get().all(block_special_file::id)`` finds it, and so does ``resolve(block_special_file::id, "ram0")``, but a caller holding only a bare ``device&`` and calling ``device.facet<block_special_file>()`` directly gets ``nullptr`` — the device object genuinely does not implement it, the driver-owned ``block_node`` does. This is the intended, documented shape (a driver's private implementation object outliving any one probe/unbind cycle only as long as ``driver_data`` keeps it alive), but it is easy to assume, incorrectly, that every published facet must also be reachable straight off the device. + +Use composition whenever the natural implementation of a facet is driver logic acting on driver-owned state (a backing memory region, an open descriptor, a piece of firmware-provided data) rather than something the device node itself should carry. + +A Complete Minimal Example +------------------------------ + +The two device-model facets already shown (``isa_signature``/``isa_claim``, ``block_special_file``) are real but entangled with ISA and boot-module machinery. The following stands alone, purely to make the four moving parts concrete: a fictional ``temperature_sensor`` facet, implemented once by inheritance and once by composition, then consumed both ways. + +.. code-block:: cpp + + // 1. Define the facet. Any module may do this; kapi::devices is untouched. + namespace kapi::sensors + { + struct temperature_sensor + { + constexpr auto static id = kapi::capabilities::facet_id{"temperature_sensor"}; + virtual ~temperature_sensor() = default; + [[nodiscard]] virtual auto read_millicelsius() const -> kstd::result<std::int32_t> = 0; + }; + } + + // 2a. Implement it by inheritance: the device type IS a temperature sensor. + struct cpu_thermal_zone final : kapi::devices::device, kapi::sensors::temperature_sensor + { + explicit cpu_thermal_zone(kstd::string const & name) : device{name} {} + + [[nodiscard]] auto read_millicelsius() const -> kstd::result<std::int32_t> override + { + return read_msr_thermal_status(); // hypothetical hardware access + } + + protected: + auto query_facet(kapi::capabilities::facet_id facet) -> void * override + { + if (facet == kapi::sensors::temperature_sensor::id) + { + return static_cast<kapi::sensors::temperature_sensor *>(this); + } + return kapi::devices::device::query_facet(facet); // never forget this line + } + }; + + // 2b. Implement it by composition: a driver-owned object stands in for a plain device. + struct i2c_probe_reading final : kapi::sensors::temperature_sensor + { + explicit i2c_probe_reading(kstd::shared_ptr<i2c::channel> channel) : m_channel{std::move(channel)} {} + [[nodiscard]] auto read_millicelsius() const -> kstd::result<std::int32_t> override + { + return m_channel->read_register(temperature_register); + } + private: + kstd::shared_ptr<i2c::channel> m_channel; + }; + + auto some_i2c_driver::probe(kapi::devices::device & device) -> kstd::result<void> + { + auto channel = /* ... acquire the i2c channel resource ... */; + auto reading = kstd::make_shared<i2c_probe_reading>(channel); + auto published = kapi::devices::publish_facet<kapi::sensors::temperature_sensor>( + device.shared_from_this(), "i2c_probe0", reading.get()); + if (!published) { return published; } + device.set_driver_data(reading); + return kstd::success(); + } + + // 3. Consume it, either way. + auto log_if_hot(kapi::devices::device & device) -> void + { + if (auto * sensor = device.facet<kapi::sensors::temperature_sensor>()) // works for 2a only + { + // ... + } + } + + auto log_every_hot_sensor() -> void + { + for (auto const & entry : kapi::devices::facet_registry::get().all(kapi::sensors::temperature_sensor::id)) + { + if (auto * sensor = entry.facet<kapi::sensors::temperature_sensor>()) // works for both 2a and 2b + { + // ... + } + } + } + +Fourteen lines define the facet and both implementations put together; the rest is exactly the boilerplate every real facet in the tree already carries. There is no fifth step where ``kapi::devices`` or ``kapi::sensors`` need to be told about each other. + +Parallels in Other Systems +------------------------------ + +Capability query without a language-provided RTTI mechanism is a solved problem, solved independently and repeatedly, because freestanding and cross-language environments hit the same wall TeachOS did. The parallels below are ordered from closest to loosest. + +COM's ``IUnknown::QueryInterface`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the closest real-world relative of facet dispatch, and almost certainly its distant conceptual ancestor. Every COM object implements ``HRESULT QueryInterface(REFIID riid, void ** ppvObject)``: given a 128-bit interface identifier (a GUID) rather than a string, it returns a pointer to that interface or an error [2]_. The shape is identical — an opaque object, a caller-supplied tag, an untyped-then-cast pointer back, no shared base class needed to know about every possible interface in advance. The differences are informative rather than superficial: COM's tags are GUIDs, generated to be globally, statistically unique, specifically to avoid the collision risk a string tag carries (see *Drawbacks*, below); ``QueryInterface`` is reference-counted (``AddRef``/``Release``) as part of the same call, because COM objects can be shared across process and apartment boundaries in ways a kernel device tree is not; and COM has a formal rule that interface identity, once shipped, can never change shape — a modified interface gets a new GUID rather than breaking existing implementers. TeachOS has no equivalent discipline yet (also noted below). + +UEFI Protocols +~~~~~~~~~~~~~~~~ + +UEFI firmware — itself a freestanding C environment without RTTI — solves precisely TeachOS's problem the same way. Every UEFI protocol is identified by a GUID; ``EFI_BOOT_SERVICES::LocateProtocol()``/``HandleProtocol()``/``OpenProtocol()`` ask a handle (UEFI's rough analogue of a ``device``) whether it implements a given protocol and hand back a pointer to a plain struct of function pointers if so [3]_. A UEFI driver "publishing" a protocol on a handle via ``InstallProtocolInterface()`` is structurally the same act as ``facet_registry::publish()`` recording a device-facet pair. This is a strong, direct precedent for the whole shape of the mechanism, including its motivation: no RTTI, no exceptions, an open set of capabilities defined by whoever needs them. + +FreeBSD's ``kobj(9)`` +~~~~~~~~~~~~~~~~~~~~~~~ + +FreeBSD's kernel object system, which underlies ``newbus`` (already TeachOS's model for ``bus_protocol::match()``'s priority scheme), gives every driver class a dynamically dispatchable method table keyed by a ``kobj_method_t`` descriptor rather than by fixed vtable slot position, letting an object answer "do I implement this specific operation" at runtime and letting a subclass override individual operations without redeclaring the whole interface [4]_. It is a lower-level, more C-flavored mechanism than facet dispatch — object-oriented C via explicit method tables rather than C++ virtual dispatch underneath — but the goal (open-ended, per-object-class operation sets, resolved by identity rather than by a fixed struct layout) is the same one facet dispatch exists to serve, and it is worth knowing this precedent exists in the exact BSD lineage the device model already draws its bus-matching priority scheme from. + +Linux's Driver Core — a Deliberate Non-Parallel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is worth being precise about where the parallel *breaks*, because Linux is the system TeachOS students will read next. Linux's ``struct device`` has no generic "ask me if I support X" entry point at all. Capability there is almost entirely static and compile-time: a ``struct net_device`` is reached by *containment* — a subsystem-specific struct embeds a ``struct device`` and the driver gets back to its own type via ``container_of()``, a macro that computes an enclosing struct's address from a member pointer and a compile-time offset, trusting the caller to know the real type. Where Linux *does* have a runtime capability-style question, it is answered by a fixed, subsystem-specific operations struct (``file_operations``, ``block_device_operations``) assigned once, not queried per-capability. The one place Linux's driver core does resemble ``bus_protocol::match()`` closely is ``struct bus_type::match(struct device *, struct device_driver *)`` — the exact same "does this driver claim this device" question, at the exact same layer [5]_. TeachOS's facet-based ``query_facet()`` generalizes what Linux left as ad-hoc, per-subsystem convention (``container_of`` here, an ops-struct there) into one uniform primitive — a deliberate design choice to make in a *teaching* kernel, since it gives students one concept to learn instead of several conventions to infer from context, even though it costs something Linux's approach doesn't (see *Drawbacks*). + +Rust's ``dyn Any`` and ``downcast_ref`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a more modern comparison: Rust's standard library ``Any`` trait lets a caller holding a ``&dyn Any`` attempt ``downcast_ref::<ConcreteType>()``, which succeeds only if the underlying value's runtime ``TypeId`` matches [6]_. This is the checked-downcast idea `dynamic_cast` provides in ordinary C++ — and which TeachOS specifically cannot use — reappearing in a language that does have RTTI-equivalent metadata (``TypeId`` is derived from the type system, not hand-assigned), which is precisely the difference from facet dispatch worth noting: Rust's tag is compiler-guaranteed unique per type; TeachOS's ``facet_id`` is a human-chosen string, with everything that implies about collision risk. + +The Expression Problem +~~~~~~~~~~~~~~~~~~~~~~~~ + +Stepping back from specific systems: facet dispatch is a concrete answer to a classic tension in programming-language design usually called the *expression problem* — can a system let you add new *data variants* (new device/driver types) and new *operations over them* (new facets) independently, without editing existing code either way? A closed ``std::variant<block_device, char_device, net_device>`` with ``std::visit`` makes adding an operation trivial (write one visitor) but adding a new variant requires touching every existing visitor. Facet dispatch takes the opposite trade familiar from classic object-oriented design: adding a new facet type costs nothing to existing devices (they simply never implement it, and ``has_facet()`` says so honestly); adding a new device type costs nothing to existing facets (it implements whichever ones apply). What facet dispatch does *not* give you for free is the ``std::visit`` side: there is no way to mechanically enumerate "every facet a given device implements" the way a closed variant's visitor can be exhaustively checked by the compiler — see *Drawbacks*. + +Benefits +----------- + +* **No RTTI, by construction.** The entire mechanism is ordinary virtual dispatch plus a manually-maintained tag comparison — it compiles and works identically whether or not ``-fno-rtti`` is set, which is exactly the freestanding constraint that motivated it. +* **The capability set is genuinely open.** ``kapi::devices::device``, ``driver``, and ``bus`` do not, and structurally cannot, know about ``isa_signature``, ``block_special_file``, or any other facet defined outside their own headers. A new facet is a new header, not a change to the device layer. +* **One primitive, reused everywhere.** Single-object capability queries (``device.facet<T>()``), symmetric driver-side identification (``driver.facet<T>()``), bus matching (``bus_protocol::match()`` querying both), and whole-tree capability indexing (``facet_registry``) are four *uses* of one mechanism, not four separately-designed ones. This matters specifically for a teaching codebase: a student who understands ``query_facet()`` once has already understood how driver matching, structural tree queries, and devfs population all work underneath. +* **Composition is a first-class option, not a workaround.** The second ``publish()`` overload means a facet's implementation does not have to be baked into the device type — a driver-owned object, constructed from data the device itself never needs to see (a boot module, an I2C channel), can stand in cleanly. +* **Failure is a value, not a crash.** A missing facet is ``nullptr``, checked with an ``if``, exactly like any other optional value in the codebase — never a panic reachable from a userspace-triggered path, which was a real, fixed defect in the pre-facet design. +* **Debuggable by inspection.** Because ``facet_id`` is string-backed, a facet's name prints directly in a debugger or a log line without a lookup table — a real value for teaching, even though it is not free (next section). + +Drawbacks and Limitations +----------------------------- + +Stated as plainly as the benefits above, because a design write-up that only lists advantages is not documenting an architecture — it is advertising one. + +* **String-keyed identity has no uniqueness guarantee.** ``facet_id`` equality is ``std::string_view`` content equality. Two unrelated facets that happen to share a literal — a typo, a copy-pasted ``id`` line, or an honest independent choice of the same short word by two different students — become, from the mechanism's point of view, *the same facet*. A ``query_facet()`` handling one of them will hand back a pointer, ``static_cast`` will "succeed" in the sense of not crashing, and the caller will silently operate on an object through the wrong type's vtable. Nothing in the type system catches this; it is a runtime, likely intermittent, memory-safety bug. Unlike COM's GUIDs (128 bits, generated to make an accidental collision practically impossible) or Rust's compiler-derived ``TypeId``, there is currently no mechanism in TeachOS enforcing that two ``facet_id`` values with the same name really do refer to the same facet, or catching it when they do not. +* **The delegation chain is manual and unenforced.** Every ``query_facet()`` override that does not recognize a requested id must remember to fall through to its base class's ``query_facet()``. Forgetting this (easy to do — it compiles cleanly either way) silently makes every facet contributed by the base class unreachable through that subtype, with no compiler diagnostic and no crash — only a ``has_facet()`` that quietly returns false where it should not, discovered (if it is discovered at all) as a device that mysteriously never matches a driver it should have. +* **No enumeration or reflection.** There is no way to ask a ``device`` or a ``facet_registry::entry`` "what facets do you support" in general — only "do you support *this specific* facet", one id at a time. A caller must already know which tag to ask for. This is the direct cost of the expression-problem trade-off described above: the operation "list every facet a device has" is not something the mechanism was designed to answer, and adding it would need either an explicit per-type registration list (defeating the "device layer knows nothing about facets" property) or a hand-maintained catalogue kept separately from every facet definition, with the drift risk that implies. +* **Dispatch cost is linear in the number of facets a concrete type supports**, not constant. Each ``query_facet()`` override is a manual if/else-if chain; a type supporting five facets does up to five string-view comparisons per query, whereas a real vtable slot is one indirect call regardless of how many other virtual functions the type has. For device counts and facet counts in a teaching kernel this is immaterial, but it is a genuine, quantifiable difference from the polymorphism this mechanism deliberately avoids, worth naming rather than glossing over. +* **No interface-versioning discipline yet.** Changing the shape of an existing facet (adding a pure virtual method, changing a signature) breaks every type that implements it, exactly as with any C++ abstract base class — facet dispatch does nothing to soften this. COM's answer (never modify a shipped interface; mint a new GUID for a new shape, and let objects implement both old and new side by side) is a real, well-tested discipline for exactly this problem that TeachOS has not yet adopted. Today, changing a facet's shape means finding and updating every implementer by hand. +* **Inheritance- and composition-published facets are not equally discoverable.** As shown above, a composed facet (``ram_disk``'s ``block_node``) is invisible to ``device.facet<T>()`` and reachable only through the registry, while an inherited facet is reachable both ways. This asymmetry is intentional and documented here, but it is exactly the kind of thing a newcomer gets wrong once before internalizing it. +* **Thread-safety is the caller's problem, not the mechanism's.** ``query_facet()`` itself performs no synchronization; a facet pointer obtained from a device is only as valid as whatever else is happening to that device concurrently (unbinding, teardown). ``facet_registry`` protects its own internal bookkeeping with a lock and hands back only ``weak_ptr``-backed handles for exactly this reason, but a raw ``T *`` obtained via ``device.facet<T>()`` carries no such protection on its own. The project's general locking primitive, ``kapi::tracked_mutex`` (``kapi/kapi/tracked_mutex.hpp``), is available for a caller that needs to hold a facet pointer across a window where the underlying device could otherwise be torn down; this brief does not attempt to restate the project's broader locking discipline. + +When (Not) to Reach for a Facet +----------------------------------- + +As a rule of thumb for anyone extending the device model: + +* Define a **new facet** when the question is "does this device/driver support capability X" and X is meaningful across more than one concrete type, or across a boundary the answering code should not need to know the concrete type to cross (a bus matching against arbitrary future drivers; the VFS asking arbitrary devices whether they are block-capable). +* Do **not** define a facet for behavior that is entirely private to one driver's own internal state — an ordinary member function is simpler, faster, and just as correct when nothing outside that driver ever needs to ask the question. +* Do **not** reuse an existing facet's ``id`` string for an unrelated purpose, and do not pick a generic single word without a namespacing prefix if there is any realistic chance of collision — the failure mode is silent, as described above. +* Remember the delegation line. If a type overrides ``query_facet()``, the last line of that override — for every id it does not itself recognize — must be a call to the immediate base class's ``query_facet()``. + +References +------------- + +.. [1] Stroustrup, B. and Sutter, H. (eds.), *C++ Core Guidelines*, section on RTTI and freestanding implementations; ISO/IEC, *Programming languages — C++*, clause on freestanding implementations (RTTI is an optional, hosted-implementation facility). `Online <https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines>`_. + +.. [2] Microsoft, "IUnknown::QueryInterface," *Component Object Model (COM) documentation*. `Online <https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)>`_. + +.. [3] UEFI Forum, Inc., *Unified Extensible Firmware Interface (UEFI) Specification*, section 7.3, "Protocol Handler Services" (``LocateProtocol``, ``HandleProtocol``, ``OpenProtocol``, ``InstallProtocolInterface``). `Online <https://uefi.org/specifications>`_. + +.. [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 the kernel object system (``kobj(9)``) and ``newbus``. See also the FreeBSD ``kobj(9)`` manual page. `Online <https://man.freebsd.org/cgi/man.cgi?query=kobj&sektion=9>`_. + +.. [5] Corbet, J., Rubini, A., and Kroah-Hartman, G., *Linux Device Drivers*, 3rd ed., O'Reilly, 2005, chapter 14, "The Linux Device Model" (``struct bus_type``, ``match()``, ``container_of()``). See also the current kernel source, ``include/linux/device/bus.h``. + +.. [6] Rust Project, ``std::any`` module documentation (``Any`` trait, ``downcast_ref``). `Online <https://doc.rust-lang.org/std/any/index.html>`_. + +.. seealso:: + + ``kapi/kapi/capabilities/facet_id.hpp``, ``kapi/kapi/devices/{device,driver,bus,bus_protocol,facet_registry}.hpp`` — the primitive and its four call sites, in full, as they exist in the tree today. + + ``arch/x86_64/arch/bus/isa.{hpp,cpp}`` and ``arch/x86_64/arch/devices/pit.{hpp,cpp}`` / ``arch/x86_64/arch/drivers/pit.{hpp,cpp}`` — the worked ISA identification example this brief quotes from directly. + + ``kernel/kernel/drivers/storage/ram_disk.cpp`` — the composed-facet example this brief quotes from directly. + + ``kapi/kapi/tracked_mutex.hpp`` — the locking primitive referenced in *Drawbacks and Limitations*, above. |
