TRY(…) Error Handling
TheTRY(...) macro is used for error propagation in the Serenity code base. The goal being to reduce the amount of boiler plate error code required to properly handle and propagate errors throughout the code base.
Any code surrounded by TRY(...) will attempt to be executed, and any error will immediately be returned from the function. If no error occurs then the result of the contents of the TRY(...) will be the result of the macro’s execution.
Our
TRY(...) macro functions similarly to the ? operator in Rust.Example from LibGfx:
Example from the Kernel:
MUST(…) Error Handling
TheMUST(...) macro is similar to TRY(...) except the macro enforces that the code run inside the macro must succeed, otherwise we assert.
Example:
Fallible Constructors
The usual C++ constructors are incompatible with SerenityOS’s method of handling errors, as potential errors are passed using theErrorOr return type. As a replacement, classes that require fallible operations during their construction define a static function that is fallible instead.
This fallible function (which should usually be named create) will handle any errors while preparing arguments for the internal constructor and run any required fallible operations after the object has been initialized. The resulting object is then returned as ErrorOr<T> or ErrorOr<NonnullOwnPtr<T>>.
Example:
The serenity_main(…) Program Entry Point
Serenity has moved to a pattern where executables do not expose a normal C main function. Aserenity_main(...) is exposed instead. The main reasoning is that the Main::Arguments struct can provide arguments in a more idiomatic way that fits with the Serenity API surface area. The ErrorOr<int> likewise allows the program to propagate errors seamlessly with the TRY(...) macro, avoiding a significant amount of clunky C style error handling.
These executables are then linked with the LibMain library, which will link in the normal C int main(int, char**) function which will call into the programs serenity_main(...) on program startup.
Traditional main vs serenity_main:
In C and C++, the main function normally looks something like:serenity_main(..) is defined like this:
Intrusive Lists
Intrusive lists are common in the Kernel and in some specific cases are used in the SerenityOS userland. A data structure is said to be “intrusive” when each element holds the metadata that tracks the element’s membership in the data structure. In the case of a list, this means that every element in an intrusive linked list has a node embedded inside it. The main advantage of intrusive data structures is you don’t need to worry about handling out of memory (OOM) on insertion into the data structure. This means error handling code is much simpler than say, using aVector in environments that need to be durable to OOM.
Example from the Region class:
The common pattern for declaring an intrusive list is to add the storage for the intrusive list node as a private member. A public type alias is then used to expose the list type to anyone who might need to create it.Static Assertions of Type Size
It’s a universal pattern to usestatic_assert to validate that the size of a type matches the author’s expectations. Unfortunately when these assertions fail they don’t give you the values that actually caused the failure.
For this reason AK::AssertSize was added. It exploits the fact that the compiler will emit template argument values for compiler errors to provide debugging information. Instead of getting no information you’ll get the actual type sizes in your compiler error output.
Example:
String View Literals
AK::StringView support for operator""sv which is a special string literal operator that was added as of C++17 to enable std::string_view literals.
This allows AK::StringView to be constructed from string literals with no runtime cost to find the string length, and the data the AK::StringView points to will reside in the data section of the binary.
Example:
Source Location
C++20 addedstd::source_location, which lets you capture the callers FILE / LINE / FUNCTION etc. as a default argument to functions.
AK::SourceLocation is the implementation of this feature in SerenityOS. It’s become the idiomatic way to capture the location when adding extra debugging instrumentation, without resorting to littering the code with preprocessor macros.
To use it, you can add the AK::SourceLocation as a default argument to any function, using AK::SourceLocation::current() to initialize the default argument.
Example:
Array Types Comparison
There 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.Span<type> differs from all of these types in that it provides a view on data owned by somebody else. The four types mentioned below all own their data, but they can provide Spans which view all or part of their data.type[] (C-style arrays)
C-style arrays are generally discouraged. They are only used for the implementation of other collections or in specific circumstances.Array<type>
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.
Vector<type>
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.
FixedArray<type>
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 in its constructor and destructor.