Serenity Operating System
at master 341 lines 12 kB view raw view rendered
1# SerenityOS Patterns 2 3## Introduction 4 5Over time numerous reoccurring patterns have emerged from or were adopted by 6the serenity code base. This document aims to track and describe them, so they 7can be propagated further and the code base can be kept consistent. 8 9## `TRY(...)` Error Handling 10 11The `TRY(..)` macro is used for error propagation in the serenity code base. 12The goal being to reduce the amount of boiler plate error code required to 13properly handle and propagate errors throughout the code base. 14 15Any code surrounded by `TRY(..)` will attempt to be executed, and any error 16will immediately be returned from the function. If no error occurs then the 17result of the contents of the TRY will be the result of the macro's execution. 18 19### Examples: 20 21Example from LibGUI: 22 23```cpp 24#include <AK/Try.h> 25 26... snip ... 27 28ErrorOr<NonnullRefPtr<Menu>> Window::try_add_menu(String name) 29{ 30 auto menu = TRY(m_menubar->try_add_menu({}, move(name))); 31 if (m_window_id) { 32 menu->realize_menu_if_needed(); 33 ConnectionToWindowServer::the().async_add_menu(m_window_id, menu->menu_id()); 34 } 35 return menu; 36} 37``` 38 39Example from the Kernel: 40 41```cpp 42#include <AK/Try.h> 43 44... snip ... 45 46ErrorOr<Region*> AddressSpace::allocate_region(VirtualRange const& range, StringView name, int prot, AllocationStrategy strategy) 47{ 48 VERIFY(range.is_valid()); 49 OwnPtr<KString> region_name; 50 if (!name.is_null()) 51 region_name = TRY(KString::try_create(name)); 52 auto vmobject = TRY(AnonymousVMObject::try_create_with_size(range.size(), strategy)); 53 auto region = TRY(Region::try_create_user_accessible(range, move(vmobject), 0, move(region_name), prot_to_region_access_flags(prot), Region::Cacheable::Yes, false)); 54 TRY(region->map(page_directory())); 55 return add_region(move(region)); 56} 57``` 58 59Note: Our `TRY(...)` macro functions similarly to the `?` [operator in rust](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator). 60 61## `MUST(...)` Error Handling 62 63The `MUST(...)` macro is similar to `TRY(...)` except the macro enforces that 64the code run inside the macro must succeed, otherwise we assert. 65 66Note that `MUST(...)` should not be used as a replacement for `TRY(...)` in cases where error propagation is not (currently) possible. 67Instead, the `release_value_but_fixme_should_propagate_errors()` method of `ErrorOr<>` should be used to retrieve the value 68and to mark the location for future improvement. `MUST(...)` is reserved for cases where we determine through other circumstances that it 69should not be possible for the code inside the macro to fail or if a failure is serious enough that the program _needs_ to crash. 70 71### Example: 72 73```cpp 74#include <AK/Vector.h> 75 76... snip ... 77 78ErrorOr<void> insert_one_to_onehundred(Vector<int>& vector) 79{ 80 TRY(vector.try_ensure_capacity(vector.size() + 100)); 81 82 for (int i = 1; i <= 100; i++) { 83 // We previously made sure that we allocated enough space, so the append operation shouldn't ever fail. 84 MUST(vector.try_append(i)); 85 } 86 87 return {}; 88} 89``` 90 91## Fallible Constructors 92 93The usual C++ constructors are incompatible with SerenityOS' method of handling errors, 94as potential errors are passed using the `ErrorOr` return type. As a replacement, classes 95that require fallible operations during their construction define a static function that 96is fallible instead. 97 98This fallible function (which should usually be named `create`) will handle any errors while 99preparing arguments for the internal constructor and run any required fallible operations after 100the object has been initialized. The resulting object is then returned as `ErrorOr<T>` or 101`ErrorOr<NonnullOwnPtr<T>>`. 102 103### Example: 104 105```cpp 106class Decompressor { 107public: 108 static ErrorOr<NonnullOwnPtr<Decompressor>> create(NonnullOwnPtr<Core::Stream::Stream> stream) 109 { 110 auto buffer = TRY(CircularBuffer::create_empty(32 * KiB)); 111 auto decompressor = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Decompressor(move(stream), move(buffer)))); 112 TRY(decompressor->initialize_settings_from_header()); 113 return decompressor; 114 } 115 116... snip ... 117 118private: 119 Decompressor(NonnullOwnPtr<Core::Stream::Stream> stream, CircularBuffer buffer) 120 : m_stream(move(stream)) 121 , m_buffer(move(buffer)) 122 { 123 } 124 125 CircularBuffer m_buffer; 126 NonnullOwnPtr<Core::Stream::Stream> m_stream; 127} 128``` 129 130## The `serenity_main(..)` program entry point 131 132Serenity has moved to a pattern where executables do not expose a normal C 133main function. A `serenity_main(..)` is exposed instead. The main reasoning 134is that the `Main::Arguments` struct can provide arguments in a more idiomatic 135way that fits with the serenity API surface area. The ErrorOr<int> likewise 136allows the program to propagate errors seamlessly with the `TRY(...)` macro, 137avoiding a significant amount of clunky C style error handling. 138 139These executables are then linked with the `LibMain` library, which will link in 140the normal C `int main(int, char**)` function which will call into the programs 141`serenity_main(..)` on program startup. 142 143The creation of the pattern was documented in the following video: 144[OS hacking: A better main() for SerenityOS C++ programs](https://www.youtube.com/watch?v=5PciKJW1rUc) 145 146### Examples: 147 148A function `main(..)` would normally look something like: 149 150```cpp 151int main(int argc, char** argv) 152{ 153 return 0; 154} 155``` 156 157Instead, `serenity_main(..)` is defined like this: 158 159```cpp 160#include <LibMain/Main.h> 161 162ErrorOr<int> serenity_main(Main::Arguments arguments) 163{ 164 return 0; 165} 166``` 167 168## Intrusive Lists 169 170[Intrusive lists](https://www.data-structures-in-practice.com/intrusive-linked-lists/) are common in the Kernel and in some specific cases 171are used in the SerenityOS userland. A data structure is said to be 172"intrusive" when each element holds the metadata that tracks the 173element's membership in the data structure. In the case of a list, this 174means that every element in an intrusive linked list has a node embedded 175inside it. The main advantage of intrusive 176data structures is you don't need to worry about handling out of memory (OOM) 177on insertion into the data structure. This means error handling code is 178much simpler than say, using a `Vector` in environments that need to be durable 179to OOM. 180 181The common pattern for declaring an intrusive list is to add the storage 182for the intrusive list node as a private member. A public type alias is 183then used to expose the list type to anyone who might need to create it. 184Here is an example from the `Region` class in the Kernel: 185 186```cpp 187class Region final 188 : public Weakable<Region> { 189 190public: 191 192... snip ... 193 194private: 195 bool m_syscall_region : 1 { false }; 196 197 IntrusiveListNode<Region> m_memory_manager_list_node; 198 IntrusiveListNode<Region> m_vmobject_list_node; 199 200public: 201 using ListInMemoryManager = IntrusiveList<&Region::m_memory_manager_list_node>; 202 using ListInVMObject = IntrusiveList<&Region::m_vmobject_list_node>; 203}; 204``` 205 206You can then use the list by referencing the public type alias like so: 207 208```cpp 209class MemoryManager { 210 211... snip ... 212 213 Region::ListInMemoryManager m_kernel_regions; 214 Vector<UsedMemoryRange> m_used_memory_ranges; 215 Vector<PhysicalMemoryRange> m_physical_memory_ranges; 216 Vector<ContiguousReservedMemoryRange> m_reserved_memory_ranges; 217}; 218``` 219 220## Static Assertions of the size of a type 221 222It's a universal pattern to use `static_assert` to validate the size of a 223type matches the author's expectations. Unfortunately when these assertions 224fail they don't give you the values that actually caused the failure. This 225forces one to go investigate by printing out the size, or checking it in a 226debugger, etc. 227 228For this reason `AK::AssertSize` was added. It exploits the fact that the 229compiler will emit template argument values for compiler errors to provide 230debugging information. Instead of getting no information you'll get the actual 231type sizes in your compiler error output. 232 233Example Usage: 234 235```cpp 236#include <AK/StdLibExtras.h> 237 238struct Empty { }; 239 240static_assert(AssertSize<Empty, 1>()); 241``` 242 243## String View Literals 244 245`AK::StringView` support for `operator"" sv` which is a special string literal operator that was added as of 246[C++17 to enable `std::string_view` literals](https://en.cppreference.com/w/cpp/string/basic_string_view/operator%22%22sv). 247 248```cpp 249[[nodiscard]] ALWAYS_INLINE constexpr AK::StringView operator"" sv(const char* cstring, size_t length) 250{ 251 return AK::StringView(cstring, length); 252} 253``` 254 255This allows `AK::StringView` to be constructed from string literals with no runtime 256cost to find the string length, and the data the `AK::StringView` points to will 257reside in the data section of the binary. 258 259Example Usage: 260```cpp 261#include <AK/String.h> 262#include <AK/StringView.h> 263#include <LibTest/TestCase.h> 264 265TEST_CASE(string_view_literal_operator) 266{ 267 StringView literal_view = "foo"sv; 268 String test_string = "foo"; 269 270 EXPECT_EQ(literal_view.length(), test_string.length()); 271 EXPECT_EQ(literal_view, test_string); 272} 273``` 274 275## Source Location 276 277C++20 added std::source_location, which lets you capture the 278callers __FILE__ / __LINE__ / __FUNCTION__ etc as a default 279argument to functions. 280See: https://en.cppreference.com/w/cpp/utility/source_location 281 282`AK::SourceLocation` is the implementation of this feature in 283SerenityOS. It's become the idiomatic way to capture the location 284when adding extra debugging instrumentation, without resorting to 285littering the code with preprocessor macros. 286 287To use it, you can add the `AK::SourceLocation` as a default argument 288to any function, using `AK::SourceLocation::current()` to initialize the 289default argument. 290 291Example Usage: 292```cpp 293#include <AK/SourceLocation.h> 294#include <AK/StringView.h> 295 296static StringView example_fn(const SourceLocation& loc = SourceLocation::current()) 297{ 298 return loc.function_name(); 299} 300 301int main(int, char**) 302{ 303 return example_fn().length(); 304} 305``` 306 307If you only want to only capture `AK::SourceLocation` data with a certain debug macro enabled, avoid 308adding `#ifdef`'s to all functions which have the `AK::SourceLocation` argument. Since SourceLocation 309is just a simple struct, you can just declare an empty class which can be optimized away by the 310compiler, and alias both to the same name. 311 312Example Usage: 313 314```cpp 315 316#if LOCK_DEBUG 317# include <AK/SourceLocation.h> 318#endif 319 320#if LOCK_DEBUG 321using LockLocation = SourceLocation; 322#else 323struct LockLocation { 324 static constexpr LockLocation current() { return {}; } 325 326private: 327 constexpr LockLocation() = default; 328}; 329#endif 330``` 331 332## `type[]` vs. `Array<type>` vs. `Vector<type>` vs. `FixedArray<type>` 333 334There are four "contiguous list" / array-like types, including C-style arrays themselves. They share a lot of their API, but their use cases are all slightly different, mostly relating to how they allocate their data. 335 336Note that `Span<type>` differs from all of these types in that it provides a *view* on data owned by somebody else. The four types mentioned above all own their data, but they can provide `Span`'s which view all or part of their data. For APIs that aren't specific to the kind of list and don't need to handle resizing in any way, `Span` is a good choice. 337 338* C-style arrays are generally discouraged (and this also holds for pointer+size-style arrays when passing them around). They are only used for the implementation of other collections or in specific circumstances. 339* `Array` is a thin wrapper around C-style arrays similar to `std::array`, where the template arguments include the size of the array. It allocates its data inline, just as arrays do, and never does any dynamic allocations. 340* `Vector` is similar to `std::vector` and represents a dynamic resizable array. For most basic use cases of lists, this is the go-to collection. It has an optional inline capacity (the second template argument) which will allocate inline as the name suggests, but this is not always used. If the contents outgrow the inline capacity, Vector will automatically switch to the standard out-of-line storage. This is allocated on the heap, and the space is automatically resized and moved when more (or less) space is needed. 341* `FixedArray` is essentially a runtime-sized `Array`. It can't resize like `Vector`, but it's ideal for circumstances where the size is not known at compile time but doesn't need to change once the collection is initialized. `FixedArray` guarantees to not allocate or deallocate except for its constructor and destructor.