diff options
| -rw-r--r-- | CODESTYLE.md (renamed from docs/code_style.md) | 291 |
1 files changed, 142 insertions, 149 deletions
diff --git a/docs/code_style.md b/CODESTYLE.md index f417a047..4401978a 100644 --- a/docs/code_style.md +++ b/CODESTYLE.md @@ -1,41 +1,45 @@ # TeachOS C++ Code Style Guide This document codifies the C++ coding idioms used throughout the TeachOS kernel. -It covers language usage, ownership and lifetime models, algorithm selection, error -propagation, class design, and naming conventions. It does **not** cover token-level -formatting, which is enforced automatically by the `.clang-format` configuration. +It covers language usage, ownership and lifetime models, algorithm selection, error propagation, class design, and naming conventions. +It does **not** cover token-level formatting, which is enforced automatically by the `.clang-format` configuration. --- ## 1. Language Standard and Vocabulary -TeachOS targets **C++23** without compiler extensions (`CMAKE_CXX_EXTENSIONS NO`). -Standard library features may be used freely where a hosted implementation is -available (i.e., in `libs/`, `kapi/`, and `kernel/`), but the project ships its own -standard library subset in `libs/kstd/`. **kstd types must be preferred over their -`std::` equivalents** wherever a kstd equivalent exists. - -| Concept | Preferred | Avoid | -|---|---|---| -| Dynamic array | `kstd::vector<T>` | `std::vector<T>` | -| String (owning) | `kstd::string` | `std::string` | -| String view | `std::string_view` | (kstd has no alias; use `std::string_view` directly) | +TeachOS targets **C++23** without compiler extensions. +Standard library features may be used freely in tests only. +All other parts of the codebase rely on a freestanding variant of the standard library as shipped with the toolchain. +Types that are not part of the toolchain's freestanding standard library are provided by the `kstd` library. +Below is an overview of `kstd` replacements to be used in the **non-test** kernel code, including the bundled support libraries. +This list is does not claim completeness. + +| Concept | Preferred | Replaces | +|--------------------|-------------------------|---------------------------------------| +| Dynamic array | `kstd::vector<T>` | `std::vector<T>` | +| Strings | `kstd::string` | `std::string` | | Non-owning pointer | `kstd::observer_ptr<T>` | raw `T*` for ownership-neutral access | -| Shared ownership | `kstd::shared_ptr<T>` | `std::shared_ptr<T>` | -| Unique ownership | `kstd::unique_ptr<T>` | `std::unique_ptr<T>` | -| Failable results | `kstd::result<T>` | exceptions, output parameters, `std::optional` for errors | -| Printing | `kstd::println(...)` | `std::println(...)`, `printf` | +| Shared ownership | `kstd::shared_ptr<T>` | `std::shared_ptr<T>` | +| Unique ownership | `kstd::unique_ptr<T>` | `std::unique_ptr<T>` | +| Printing | `kstd::println(...)` | `std::println(...)`, ... | +| String Formatting | `kstd::format(...)` | `std::format(...)`, ... | -`std::string_view`, `std::span`, `std::array`, `std::optional`, `std::byte`, and the -`std::ranges` and `std::views` namespaces are used directly from the standard library -because they have no kstd equivalents. +Common standard library parts that are available in the freestanding implementation include but are not limited to: +- `std::string_view` +- `std::span` +- `std::array` +- `std::optional` +- `std::byte` +- `std::ranges` +- `std::views` ---- +These parts may be used freely throughout the codebase. ## 2. Function Declarations — Trailing Return Types -**All** functions and member functions use trailing return type syntax, including those -returning `void`. +**All** functions and member functions use trailing return type syntax, including those returning `void`. +See the following code snippet for examples: ```cpp // Correct @@ -49,11 +53,8 @@ bool bitmap_is_set(std::span<std::byte const> bitmap, std::size_t index); void init(); ``` -This rule applies to: free functions, member functions, lambdas with explicit return -types, and virtual functions. The only exception is constructors and destructors, -which have no return type at all. - ---- +This rule applies to: free functions, member functions, lambdas with explicit return types, and virtual functions. +The only exception is constructors and destructors, which have no return type at all. ## 3. Parameter Passing Conventions @@ -61,31 +62,36 @@ The choice of passing convention encodes intent and must be consistent. ### 3.1 View and cheaply copyable types — pass by value -Types that are designed to be non-owning views or are trivially copyable must be -passed and returned **by value**. Passing them by `const &` is redundant and adds -a pointer indirection with no benefit. +Types that are designed to be non-owning views or are trivially copyable must be passed and returned **by value**. +Passing them by `const &` is redundant and adds a pointer indirection with no benefit. -This applies to: +This includes but is not limited to: - `std::string_view` - `std::span<T>` - `kstd::observer_ptr<T>` -- `kstd::bytes`, `kstd::pages` and similar unit wrappers +- `kstd::bytes` and similar unit types - `kapi::capabilities::facet_id` -- `kapi::memory::page`, `kapi::memory::frame`, `kapi::memory::physical_address`, `kapi::memory::linear_address` +- `kapi::memory::page` +- `kapi::memory::frame` +- `kapi::memory::physical_address` +- `kapi::memory::linear_address` + +See the following code snippet for examples: ```cpp // Correct -auto resolve(kapi::capabilities::facet_id id, std::string_view name) -> void *; +auto resolve(kapi::capabilities::facet_id id, std::string_view name) -> std::observer_ptr<void>; auto has(std::span<std::byte const> data) -> bool; // Wrong -auto resolve(kapi::capabilities::facet_id const & id, std::string_view const & name) -> void *; +auto resolve(kapi::capabilities::facet_id const & id, std::string_view const & name) -> std::observer_ptr<void>; ``` -### 3.2 Large or non-trivial types — pass by `const &` +### 3.2 Large or non-trivially copyable types — pass by `const &` + +For types that own heap memory or are non-trivially copyable, use `const &` when the callee does not take ownership. -For types that own heap memory or are non-trivially copyable, use `const &` when the -callee does not take ownership. +See the following code snippet for examples: ```cpp auto do_publish(kstd::string const & name) -> kstd::result<void>; @@ -94,13 +100,14 @@ auto add_child(kstd::string const & child_name) -> void; ### 3.3 Sink parameters — pass by value and move -When a function is designed to take **ownership** of an argument, accept it by value -and move it into its destination. This makes the transfer explicit at the call site. +When a function is designed to take **ownership** of an argument, accept it by value and move it into its destination. +This makes the transfer explicit at the call site. + +See the following code snippet for examples: ```cpp // In the header auto add_child(kstd::shared_ptr<device> child) -> void; -auto do_publish(kstd::shared_ptr<device> device, kstd::string name, ...) -> kstd::result<void>; // In the implementation auto bus::add_child(kstd::shared_ptr<device> child) -> void @@ -111,28 +118,32 @@ auto bus::add_child(kstd::shared_ptr<device> child) -> void ### 3.4 Mutable subsystem references — pass by non-const reference -Services and subsystems that are mutated in-place (e.g., `page_mapper &`, -`driver_state &`, `kapi::devices::bus &`) are passed by non-const reference. This -expresses that the function operates on a shared, mutable context. +Services and subsystems that are mutated in-place (e.g., `page_mapper &`, `driver_state &`, `kapi::devices::bus &`) are passed by non-const reference. +This expresses that the function operates on a shared, mutable context. + +See the following code snippet for examples: ```cpp auto remap_kernel(kapi::memory::page_mapper & mapper) -> void; auto add_directory_entry(inode & directory, inode & child, driver_state & state, write_batch & batch) -> kstd::result<void>; ``` ---- - ## 4. Error Handling +TeachOS kernel code, and all library code used by it, cannot make use of exceptions. +Use of exception related keywords, `try`, `catch`, `throw` in kernel code will cause compilation to fail. +However, exceptions are allowed in test code. + ### 4.1 Recoverable errors — `kstd::result<T>` -Functions that can fail in an expected, recoverable way must return `kstd::result<T>` -(an alias for `std::expected<T, kstd::error_code>`). Use the `kstd::success()` and -`kstd::failure()` helpers consistently. +Functions that can fail in an expected, recoverable way must return `kstd::result<T>`. +Use the `kstd::success()` and `kstd::failure()` helpers consistently. +Define per-subsystem error code if necessary. + +See the following code snippet for examples: ```cpp -auto mount(kstd::shared_ptr<inode> parent) - -> kstd::result<std::pair<kstd::shared_ptr<inode>, state *>>; +auto mount(kstd::shared_ptr<inode> parent) -> kstd::result<std::pair<kstd::shared_ptr<inode>, state *>>; // In the implementation if (!device) @@ -144,6 +155,8 @@ return kstd::success(result_value); Callers must check the result before using its value. The idiomatic check is: +See the following code snippet for examples: + ```cpp auto result = some_function(); if (!result) @@ -153,17 +166,18 @@ if (!result) // use *result ``` -Monadic composition (`transform`, `and_then`, `or_else`) is preferred over -manual if-check-and-return chains when it produces clearer code. +Monadic composition (`transform`, `and_then`, `or_else`) is preferred over manual if-check-and-return chains when it produces clearer code. ### 4.2 Unrecoverable errors — `kapi::system::panic` Violations of kernel invariants (e.g., a subsystem used before being initialized, -OOM during boot) call `kapi::system::panic(...)`. `panic` is `[[noreturn]]`. It must -not be used for recoverable errors. +OOM during boot) call `kapi::system::panic(...)`. +`panic` is `[[noreturn]]`. +It must not be used for recoverable errors. + +Log prefix convention: `[SUBSYSTEM:TAG] message`. Examples: `[OS:DEV]`, `[OS:VFS]`, `[ARCH:DRV]`. -Log prefix convention: `[SUBSYSTEM:TAG] message`. Examples: `[OS:DEV]`, -`[OS:VFS]`, `[ARCH:DRV]`. +See the following code snippet for examples: ```cpp if (!instance) @@ -172,53 +186,40 @@ if (!instance) } ``` -### 4.3 No exceptions - -The kernel does not use C++ exceptions. Do not write `throw` or `try`/`catch` in -kernel code. - ---- - ## 5. Ownership and Lifetime ### 5.1 Shared ownership — `kstd::shared_ptr` -Use `kstd::shared_ptr<T>` when a resource is co-owned by multiple subsystems and -its lifetime must be extended by any of them (e.g., `device`, `inode`, `dentry`). +Use `kstd::shared_ptr<T>` when a resource is co-owned by multiple subsystems and its lifetime must be extended by any of them (e.g., `device`, `inode`, `dentry`). Weak back-references that must not extend lifetime use `kstd::weak_ptr<T>`. ### 5.2 Non-owning references — `kstd::observer_ptr` and raw references -Use `kstd::observer_ptr<T>` to express a non-owning pointer where null is a valid -state and the holder has no say in the lifetime of the target. Use a raw reference -(`T &` or `T const &`) when null is not valid and the reference is short-lived (i.e., -a function parameter or a local alias). +Use `kstd::observer_ptr<T>` to express a non-owning pointer where null is a valid state and the holder has no say in the lifetime of the target. +Use a raw reference (`T &` or `T const &`) when null is not valid and the reference is short-lived (i.e. a function parameter or a local alias). -Never use raw `T *` to mean "sometimes I own this, sometimes I don't". Ownership must -be expressed unambiguously through the pointer type. +Never use raw `T *` to mean "sometimes I own this, sometimes I don't". +Ownership must be expressed unambiguously through the pointer type. -### 5.3 Driver data — `kstd::shared_ptr<void>` +### 5.3 Device driver data — `kstd::shared_ptr<void>` -Drivers attach their state to a `device` using an untyped `kstd::shared_ptr<void>` -via `device::set_driver_data`. A driver retrieves its state via `device::driver_data`. -This allows the device tree to destroy driver data automatically when the device is -released, without the device knowing the concrete driver type. - ---- +Drivers attach their state to a `device` using an untyped `kstd::shared_ptr<void>` via `device::set_driver_data`. +A driver retrieves its state via `device::driver_data`. +This allows the device tree to destroy driver data automatically when the device is released, without the device knowing the concrete driver type. ## 6. Algorithm and Range Usage ### 6.1 Prefer `std::ranges` algorithms over manual loops -When processing a range to search, filter, transform, or reduce, use the appropriate -`std::ranges` algorithm or view pipeline instead of writing a raw `for` loop. +When processing a range to search, filter, transform, or reduce, use the appropriate `std::ranges` algorithm or view pipeline instead of writing a raw `for` loop. + +See the following code snippet for examples: ```cpp // Correct -auto already_published = std::ranges::any_of( - m_entries, [&](auto const & entry) { - return entry.id() == id && entry.device().get() == device.get(); - }); +auto already_published = std::ranges::any_of(m_entries, [&](auto const & entry) { + return entry.id() == id && entry.device().get() == device.get(); +}); std::ranges::for_each(observers, [&](auto observer) { /* ... */ }); @@ -234,16 +235,15 @@ return false; ``` Acceptable uses of explicit loops include: -- Accumulation or mutation that modifies state in-place and cannot be cleanly - expressed with a ranges algorithm. +- Accumulation or mutation that modifies state in-place and cannot be cleanly expressed with a ranges algorithm. - Low-level routines dealing with raw memory arithmetic (allocators, page mappers). - Iterator-pair loops in library internals (`kstd::vector`, `kstd::basic_string`). ### 6.2 Prefer view composition over intermediate containers -Build processing pipelines using `std::views::filter`, `std::views::transform`, -`std::views::reverse`, `std::views::split`, and `std::ranges::subrange` rather than -materialising intermediate vectors. +Build processing pipelines using `std::views::filter`, `std::views::transform`, `std::views::reverse`, `std::views::split`, and `std::ranges::subrange` rather than materialising intermediate vectors. + +See the following code snippet for examples: ```cpp // Correct @@ -257,8 +257,8 @@ auto modules_view = std::ranges::subrange(begin(), end()) ### 6.3 Do not call the same function twice to avoid storing the result -If an intermediate value is needed more than once, store it in a local variable. This -applies especially to factory calls and heap allocations. +If an intermediate value is needed more than once, store it in a local variable. +This applies especially to factory calls and heap allocations. ```cpp // Wrong — double invocation, two allocations, different objects @@ -277,14 +277,13 @@ for (auto driver : descriptors) } ``` ---- - ## 7. Class and Struct Design ### 7.1 Prefer `struct` over `class` The entire codebase uses `struct` for all type definitions with explicit `private:` -sections where necessary. Do not introduce `class`. +sections where necessary. +Do not introduce `class`. ### 7.2 Member ordering within a type @@ -299,6 +298,8 @@ Follow this ordering within a `struct`: Data members are always in the `private` section and always prefixed with `m_`. +See the following code snippet for examples: + ```cpp struct facet_registry { @@ -320,9 +321,10 @@ private: ### 7.3 `explicit` on single-argument constructors -Mark every single-argument constructor `explicit` unless an implicit conversion is -intentional and documented. A deliberate implicit constructor must be accompanied by -a comment explaining the decision. +Mark every single-argument constructor `explicit` unless an implicit conversion is intentional and documented. +A deliberate implicit constructor must be accompanied by a comment explaining the decision. + +See the following code snippet for examples: ```cpp // Correct @@ -337,8 +339,9 @@ constexpr page(chunk other) : chunk{other} {} ### 7.4 Declare deleted special members explicitly -If a type is not copyable or not movable, declare the deleted special members -explicitly rather than relying on implicit suppression. +If a type is not copyable or not movable, declare the deleted special members explicitly rather than relying on implicit suppression. + +See the following code snippet for examples: ```cpp device(device const &) = delete; @@ -347,16 +350,15 @@ auto operator=(device const &) -> device & = delete; ### 7.5 Virtual destructors -Every base class with virtual member functions must have a `virtual` destructor, -defaulted if not otherwise needed. +Every base class with virtual member functions must have a `virtual` destructor, defaulted if not otherwise needed. + +See the following code snippet for examples: ```cpp virtual ~driver_descriptor() = default; virtual ~facet_registry_observer() = default; ``` ---- - ## 8. Static Singletons Several subsystems expose a single global instance via an `init()`/`get()` pair. @@ -365,8 +367,9 @@ Follow this pattern: - Store the instance in an anonymous namespace as a `constinit std::optional<T>`. - `init()` asserts the instance is not yet constructed, then `emplace()`s it. - `get()` asserts the instance exists and returns a reference to it. -- Both functions panic on violation rather than returning an error code, because - incorrect call order is a programming error, not a recoverable runtime condition. +- Both functions panic on violation rather than returning an error code, because incorrect call order is a programming error, not a recoverable runtime condition. + +See the following code snippet for examples: ```cpp namespace @@ -393,13 +396,12 @@ auto device_registry::get() -> device_registry & } ``` ---- - ## 9. Enumerations -All enumerations use `enum struct` (scoped enums), never plain `enum`. Specify the -underlying type explicitly when the representation matters (e.g., for hardware -register fields). +All enumerations use `enum struct` (scoped enums), never plain `enum`. +Specify the underlying type explicitly when the representation matters (e.g., for hardware register fields). + +See the following code snippet for examples: ```cpp // Correct @@ -414,57 +416,49 @@ enum struct state enum state { uninitialized, present, bound }; ``` ---- - ## 10. Naming -| Symbol | Convention | Example | -|---|---|---| -| Types (struct, enum) | `lower_case` | `device_registry`, `facet_id` | -| Functions and methods | `lower_case` | `add_child`, `make_instance` | -| Local variables | `lower_case` | `entry`, `block_index` | -| Private data members | `m_` prefix, `lower_case` | `m_entries`, `m_driver_data` | -| Template type parameters | `CamelCase` | `ValueType`, `FacetType` | -| Constants and constexpr variables | `lower_case` | `page_size`, `direct_block_count` | -| Type aliases | `lower_case` | `value_type`, `size_type` | -| Namespaces | `lower_case` | `kapi::devices`, `kernel::vfs` | +| Symbol | Convention | Example | +|-----------------------------------|---------------------------|-----------------------------------| +| Types (struct, enum) | `lower_case` | `device_registry`, `facet_id` | +| Functions and methods | `lower_case` | `add_child`, `make_instance` | +| Local variables | `lower_case` | `entry`, `block_index` | +| Private data members | `m_` prefix, `lower_case` | `m_entries`, `m_driver_data` | +| Template type parameters | `CamelCase` | `ValueType`, `FacetType` | +| Constants and constexpr variables | `lower_case` | `page_size`, `direct_block_count` | +| Type aliases | `lower_case` | `value_type`, `size_type` | +| Namespaces | `lower_case` | `kapi::devices`, `kernel::vfs` | -Namespaces reflect directory structure: `kernel::vfs`, `kernel::filesystems::ext2`, -`arch::devices`, etc. - ---- +Namespaces reflect directory structure: `kernel::vfs`, `kernel::filesystems::ext2`, `arch::devices`, etc. ## 11. `[[nodiscard]]` -Mark any function `[[nodiscard]]` whose return value the caller should not silently -discard. This includes in particular: +Mark any function `[[nodiscard]]` whose return value the caller should not silently discard. +This includes in particular: - All functions returning `kstd::result<T>`. - All query functions (getters, lookups) that return computed data. - Factory functions and builder utilities. +See the following code snippet for examples: + ```cpp [[nodiscard]] auto resolve(kapi::capabilities::facet_id id, std::string_view name) -> void *; [[nodiscard]] auto children() const -> kstd::vector<kstd::shared_ptr<device>>; [[nodiscard]] auto request_resource(resource_type type, std::size_t index = 0) const -> kstd::result<resource>; ``` ---- - ## 12. `constexpr` and `constinit` -Mark functions `constexpr` whenever they can be evaluated at compile time or in -constant expressions, even if they are also called at runtime. Mark module-scope -variables `constinit` to guarantee zero-initialization before any dynamic -initialization runs — important in a kernel with no well-defined global -initialization order. - ---- +Mark functions `constexpr` whenever they can be evaluated at compile time or in constant expressions, even if they are also called at runtime. +Mark module-scope variables `constinit` to guarantee zero-initialization before any dynamic initialization runs. ## 13. Documentation -Every non-trivial public type, function, and data member must be documented with a -Doxygen comment using the `//!` line style. +Every non-trivial public type, function, and data member must be documented with a Doxygen comment using the `//!` line style. + +See the following code snippet for examples: + ```cpp //! A brief one-line description. @@ -477,16 +471,15 @@ Doxygen comment using the `//!` line style. auto add_child(kstd::shared_ptr<device> child) -> void; ``` -Doxygen grouping (`@addtogroup`, `@{`, `@}`) is used to organise the API surface into -logical sections visible in generated documentation. - ---- +Doxygen grouping (`@name`, `@{`, `@}`) may be used to organise the API surface into logical sections visible in generated documentation. ## 14. Header Guards -All headers use traditional include guards, not `#pragma once`. The guard name follows -the pattern `TEACHOS_<SUBPACKAGE>_<PATH>_HPP`, where each path component is -uppercased and separators are replaced by `_`. +All headers use traditional include guards, not `#pragma once`. +The guard name follows the pattern `TEACHOS_<SUBPACKAGE>_<PATH>_HPP`, where each path component is uppercased and separators are replaced by `_`. +The only exception to this pattern are the bundled libraries, which follow the pattern `<LIBRARYNAME>_<SUBPACKAGE>_<PATH>_HPP`. + +See the following code snippet for examples: ```cpp #ifndef TEACHOS_KAPI_DEVICES_BUS_HPP |
