Skip to content

Reduce 'alloc' in Zephyr Rust interfaces #59

Open
@d3zd3z

Description

@d3zd3z

Currently, through the Fixed type, Zephyr kernel objects in Rust are wrapped in an enum that either points to a static declaration, or to a heap allocated Pin<Box<...>>. Unfortunately, this makes the constructor for these types either awkward (for static), or non-const (because of the allocation).

#57 proposes removing the enum, and replacing the object representation as the object itself paired with an atomic usize. The usize is used to ensure the objects are initialized, but allow the initialization to happen "later", presumably after most uses have finished moving the object. Using the atomic this way has similar performance to the Fixed enum above. With a minor change so that most constructors don't return a Result, but directly return the object, this allows easy static use:

static MY_SEM: Semaphore = Semaphore::new(0, u32::MAX);

with the object then being either directly referenced, or passed around via a simple &'static Semaphore type.

Object initialization difficulty

The problem results from an inherent conflict between the memory model assumed by Rust and the model used within Zephyr. Most kernel objects within Zephyr contain a "dlist", a doubly linked list, where the list points back to the originator of the list to indicate the end. As such, it is important that these objects never move in memory.

On the other hand, Rust makes a general assumption that, because of the borrow checker, the Rust language can have a solid assurance when it is the single "owner" of an object (meaning no references), it is free to move the object elsewhere in memory.

There are a few ways to solve this conflict:

  1. Require Pin for the Zephyr. For some things, where Zephyr maintains references to these kernel objects, even if there are no longer Rust references (work queues, work items, threads, and timers), Pin makes sense. It still doesn't address that Zephyr needs these objects to have a lifetime beyond any possible Rust use (mostly applies to things that are allocated). However, Pin is rather awkward to use, and often results in the need to use unsafe because the semantics of Pin inherently go beyond the normal Rust borrow checker rules.
  2. Use another mechanimsm to ensure the objects haven't moved. This is what we decide to do here.

Atomic detection

The solution in this PR is for any Zephyr objects where Zephyr does not have references to the object, unless Rust code also maintains one (Semaphores, Mutex/Condvar, Queues) is to pair each object with an atomic to indicate the state of the object. The atomic has the following values:

  • zero: Indicates the object has not yet been initialized. Rust is free to move the object in memory. Upon the first need to give the address of the object to Zephyr, it will be initialized.
  • ptr: The value of a pointer that is the same as the object. This happens after the first use detects the object needs to be initilized, and the atomic is set to the address of the Zephyr object itself.
  • differing ptr: If the object is moved by Rust, the address of the object and the value in the atomic will differ. Currently, this results in a panic, although for some objects, it may be possible to re-initialize them, since due to borrow checker rules, we know that Zephyr has no references, and Rust only has a single reference, hence there are no issues with the re-init.

It might seem a bit excessive to handle the post-first-use moves as a panic, but for the common case, it fits well with common use cases:

  • static: For objects declared statically, this allows the constructor to be const, meaning the static can just be initialized on first use.
  • StaticCell: Static Cells are initialized, possibly at runtime, once and kept static. There is a possible move from the return result of the constructor into the static. But, before a single use is possible, the object will become static and not movable.
  • Arc/Rc: These will be moved into the allocated memory, before a first use can occur, and allocations do not move.

The main use case that is not covered is where a constructor wishes to use an object before it is then moved into a larger object. An example of this is with the k_queue (Queue) that is used by the current bounded channel. The channel uses one queue as the free list. In this particular case, the Queue is still placed in a Box, and given that the memory for the allocations is also in a box, this isn't really an issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    • Status

      In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions