A scalable structure for our code
📝 This post is tangential to the original series. If you’d like to continue reading, you can find the next post here.
So far we’ve been writing our code in a single place because we were just experimenting with things.
But now, before we continue building features, we need to pause and set up a scalable foundation. Kernel projects grow quickly and without structure, they become painful to maintain. In this post, we’ll perform a few housekeeping tasks to keep our code organized, robust, and future-proof.
Nothing new conceptually today. We’re just organizing.
We’ll break this down into three sections.
Restructuring the Kernel Crate
We’ll keep our module structure fairly simple.
We’ll have a central lib.rs which exports all its submodules. Those submodules will then be used by the main.rs file under a consolidated kernel package.
That way, sharing code among different modules would be easier.
The main.rs will only act as a driver and the entry point to our kernel, and the rest of the functionality will be delegated to the lib.rs module.
To do this, we first create the kernel/src/lib.rs file, and import it in the main.rs file as a module. We’ll also add a stub init function which will grow as we add more functionality.
1#![no_std] // 👈 remember to add the #![no_std] attribute in your lib.rs file as well
2
3//! Zeno - Minimal x86-64 Operating System Kernel
4
5use bootloader_api::BootInfo;
6
7/// Initialize the Zeno OS Kernel by implementing the following:
8/// -
9pub fn init(_: &'static mut BootInfo) {
10 // we'll initialize kernel sub-systems here
11}1...
2- fn launch(_: &'static mut BootInfo) -> ! {
3+ fn launch(boot_info: &'static mut BootInfo) -> ! {
4+ kernel::init(boot_info);
5 main();
6...
Now that we’ve done that, we’ll write most of our code in separate modules, initialize them in the init function, and then use the main and launch functions to drive them.
Enforcing Code Quality with Clippy
To set our project up in such a way that it pushes us towards writing more robust, correct and idiomatic code, we’ll add some lint rules. These lint rules will allow us to catch bugs early that we’d otherwise ignore as well as allow us to discover more idiomatic Rust code. Clippy has a bunch of lint rules that focus on correctness, style, complexity, and performance.
Initially, we’ll create a very strict set of lint rules that disallow a bunch of things. We’ll later disable the rules that start to become annoyances rather than guardrails.
Add these lints at the top of your lib.rs (and additionally, your main.rs file)
Don’t worry about understanding each lint individually, you can copy this block as-is. We’ll encounter them naturally as we build features.
1#![deny(
2 clippy::pedantic,
3 clippy::all,
4 clippy::nursery,
5 clippy::await_holding_lock,
6 clippy::char_lit_as_u8,
7 clippy::checked_conversions,
8 clippy::dbg_macro,
9 clippy::debug_assert_with_mut_call,
10 clippy::doc_markdown,
11 clippy::empty_enums,
12 clippy::enum_glob_use,
13 clippy::exit,
14 clippy::expl_impl_clone_on_copy,
15 clippy::explicit_deref_methods,
16 clippy::explicit_into_iter_loop,
17 clippy::fallible_impl_from,
18 clippy::filter_map_next,
19 clippy::flat_map_option,
20 clippy::float_cmp_const,
21 clippy::fn_params_excessive_bools,
22 clippy::from_iter_instead_of_collect,
23 clippy::if_let_mutex,
24 clippy::implicit_clone,
25 clippy::imprecise_flops,
26 clippy::inefficient_to_string,
27 clippy::invalid_upcast_comparisons,
28 clippy::large_digit_groups,
29 clippy::large_stack_arrays,
30 clippy::large_types_passed_by_value,
31 clippy::let_unit_value,
32 clippy::linkedlist,
33 clippy::lossy_float_literal,
34 clippy::macro_use_imports,
35 clippy::manual_ok_or,
36 clippy::map_err_ignore,
37 clippy::map_flatten,
38 clippy::map_unwrap_or,
39 clippy::match_same_arms,
40 clippy::match_wild_err_arm,
41 clippy::match_wildcard_for_single_variants,
42 clippy::mem_forget,
43 clippy::missing_enforced_import_renames,
44 clippy::mut_mut,
45 clippy::mutex_integer,
46 clippy::needless_borrow,
47 clippy::needless_continue,
48 clippy::needless_for_each,
49 clippy::option_option,
50 clippy::path_buf_push_overwrite,
51 clippy::ptr_as_ptr,
52 clippy::rc_mutex,
53 clippy::ref_option_ref,
54 clippy::rest_pat_in_fully_bound_structs,
55 clippy::same_functions_in_if_condition,
56 clippy::semicolon_if_nothing_returned,
57 clippy::single_match_else,
58 clippy::string_add_assign,
59 clippy::string_add,
60 clippy::string_lit_as_bytes,
61 clippy::todo,
62 clippy::trait_duplication_in_bounds,
63 clippy::unimplemented,
64 clippy::unnested_or_patterns,
65 clippy::unused_self,
66 clippy::useless_transmute,
67 clippy::verbose_file_reads,
68 clippy::zero_sized_map_values,
69 clippy::redundant_pattern,
70 clippy::missing_panics_doc,
71 clippy::missing_errors_doc,
72 clippy::empty_docs,
73 clippy::missing_safety_doc,
74 future_incompatible,
75 nonstandard_style,
76 rust_2018_idioms,
77 unused
78)]Phew! That many huh?
Well, we do want to be as strict as possible. And in any case, if we feel that a lint becomes too strict for a specific piece of functionality i.e., it becomes a hindrance rather than a guardrail, we can disable it selectively.
Additionally, instead of warn, we’ve marked all the lints as deny, meaning violation of any of these lints will be considered a hard error.
It might feel too strict at first, but future-you will thank you.
As soon as you add these lints, and run the cargo clippy command, you’ll see a bunch of things popping up. Don’t worry, it’s just a bunch of warnings that we can fix by adding const in front of the init and panic_handler
💡 Making sure our editor is also on the same page (regarding these lints)
Some LSP enabled editors only run the
cargo checkcommand when checking for any issues. Unfortunately for us, that means we won’t know we’ve broken any of our lint rules unless we run thecargo clippycommand ourselves.To prevent this, we should configure our editor such that it runs the
cargo clippycommand instead, so that whenever we are in violation of any of these lints, the editor will highlight the error and prompt us to fix it immediately, instead of us having to run the command ourselves and then fixing them later.These are the editors that I personally use, so if you’re using something else, please refer to the respective documentation for your editor.
Developer Experience Improvements
To make our lives easier down the line, we’ll also install the x86_64 crate since it provides abstractions for a lot of x86-64 architecture primitives (such as register read/writes, flag set/unset, etc) so it’ll be helpful for us later down the line.
Writing inline assembly is a bit tedious and error prone. Additionally, it requires a lot of unsafe blocks in our code, which will make audits more difficult. The x86_64 crate will provide safe abstractions for these operations, making our code more readable and maintainable.
1# in the kernel directory
2cargo add x86_64While we’re at it, we should also add the hlt instruction in our launch function, to prevent the CPU from spinning indefinitely.
1fn launch(boot_info: &'static mut BootInfo) -> ! {
2 kernel::init(boot_info);
3 main();
4
5 loop {
6+ x86_64::instructions::hlt();
7 }
8}
With that, we’re all set to continue working on our OS.
We now have:
- A modular kernel architecture
- Strict lint enforcement (via Clippy)
- Architecture abstractions (via the
x86_64crate)
Let’s continue where we left off and implement Serial Output and Debugging via Port I/O.