-
Talking Tock 64: Correctly Initializing Uninit Structs
Tock needs to allocate data structures in unitialized memory. This is particularly necessary to create new processes, as Tock dynamically allocates the process control block (PCB) in process memory. Rust provides the
MaybeUninittool to enable creating types in uninitialized memory. This is a necessary feature, but when using it for the PCB, construction requires field-by-field initialization to avoid constructing an entire PCB in memory on the stack before writing it to the process’s allocated memory.One challenge with doing field-by-field struct initialization is that every field in the struct must be initialized before calling
MaybeUninit::assume_init(), or the assume init call is unsafe. For small structs (say, fewer than five fields), it is fairly straightforward to ensure during code review that all fields have been initialized. However, for something like Tock’s standard PCB, and its ~25 fields, it can be easy to miss an uninitialized field during code review. When a new field is added to the PCB struct, that field must be initialized as well everywhere the struct is created. Because we’ve told the compiler the memory isMaybeUninit, Rust will not generate an error if a field is not initialized, but the program will be unsound.To ensure a compiler error is generated if not every field of a struct is initialized in a
MaybeUninituse case, we created theinit_uninit_struct!()macro. This allows for field-by-field initialization to look like a normal struct creation. For example, consider this simple process control block struct:struct ProcessControlBlock { process_id: usize, state: ProcessState, }When we create the PCB in uninitialized memory like this:
// Get pointer to where memory has been allocated for this new process. let process_memory: *mut u8 = allocate_process_memory(); // Cast the pointer to an uninitialized pointer to the PCB. let process_control_block_memory_location: *mut MaybeUninit<ProcessControlBlock> = process_memory.cast(); // Get a reference to the PCB we can initialize. let process_uninit: &mut MaybeUninit<ProcessControlBlock> = unsafe { &mut *process_control_block_memory_location };We can use
init_uninit_struct!()to populate all fields like this:unsafe { init_uninit_struct!(process_uninit => ProcessControlBlock { process_id: 1, state: ProcessState::Unstarted, )}; }Notice the syntax looks like a normal struct initialization. However, the macro is actually creating code like the following, initializing the struct field-by-field:
let process_uptr = process_uninit.as_mut_ptr(); unsafe { (&raw mut (*process_uptr).process_id).write(1); (&raw mut (*process_uptr).state).write(ProcessState::Unstarted); }The reason this is useful is because the macro is simultaneously creating a “shadow” construction of the
ProcessControlBlockstruct with normal Rust syntax. This instantiation ofProcessControlBlockis removed by the compiler before actually creating it in the real program, but, crucially, not before the compiler pass that verifies all fields in the struct are initialized. That way, if any fields are not set in the field-by-field initialization, Rust compilation will fail!The “shadow” construction looks something like this:
if false { let _: ProcessControlBlock { process_id: todo!(), state: todo!(), }; }This ensures it is both checked for all fields, but doesn’t cause any borrow-checker errors.
After the construction, we can then call assume init knowing that every field is initialized, and that any fields added in the future will be initialized as well:
let process = unsafe { process_uninit.assume_init_mut() };Macro Implementation
See the Tock source code for the implementation.
-
Talking Tock 63: Safe DMA in Tock
Direct Memory Access (DMA) is a critical feature for increasing performance and reducing energy consumption in embedded contexts. However, from a memory-safety perspective, allowing a hardware peripheral to modify memory in a way that the compiler cannot reason about requires special care to ensure the soundness requirements expected by the compiler are maintained.
-
Talking Tock 62: Sound static global variables in Tock
In specific contexts Tock uses global
static mutvariables to share state among components. The most ubiquitous use of this sharing state between the main execution context and thepanic!()handler. When a Tock board panics, it needs to access resources used by the main kernel, including the list of processes, a reference to the MCU state, and a tool for formatting process printing data. Because panic is an exception, we need a global variable to access those resources. -
Talking Tock 61: x86 Working Group
Following on Talking Tock #60, we have established an x86 working group. This working group will steward Tock development (include userspace and tooling) related to Tock running on x86 platforms. With representation from multiple companies, the working group will help ensure x86 support grows while meeting the needs of Tock’s users.
-
Tock Strategy Workshop: Winter 2025
In March 2025, we held a virtual strategy workshop marking about a halfway point between TockWorld 7 and TockWorld 8. Our goal to discuss project direction and focus areas for the coming months—where efforts were best spent and where there might be good opportunities for collaboration. We had a group of about 20 contributors and stakeholders in the Tock project, including members of the working groups and other contributors and stakeholders representing UC San Diego, Princeton, OxidOS, Microsoft, Google, zeroRISC, and AMD.