zephyr

Module object

source
Expand description

§Zephyr Kernel Objects

Zephyr has a concept of a ‘kernel object’ that is handled a bit magically. In kernel mode threads, these are just pointers to the data structures that Zephyr uses to manage that item. In userspace, they are still pointers, but those data structures aren’t accessible to the thread. When making syscalls, the kernel validates that the objects are both valid kernel objects and that the are supposed to be accessible to this thread.

In many Zephyr apps, the kernel objects in the app are defined as static, using special macros. These macros make sure that the objects get registered so that they are accessible to userspace (at least after that access is granted).

There are also kernel objects that are synthesized as part of the build. Most notably, there are ones generated by the device tree.

§Safety

Zephyr has traditionally not focused on safety. Early versions of project goals, in fact, emphasized performance and small code size as priorities over runtime checking of safety. Over the years, this focus has changed a bit, and Zephyr does contain some additional checking, some of which is optional.

Zephyr is still constrained at compile time to checks that can be performed with the limits of the C language. With Rust, we have a much greater ability to enforce many aspects of safety at compile time. However, there is some complexity to doing this at the interface between the C world and Rust.

There are two types of kernel objects we deal with. There are kernel objects that are allocated by C code (often auto-generated) that should be accessible to Rust. These are mostly struct device values, and will be handled in a devices module. The other type are objects that application code wishes to declare statically, and use from Rust code. That is the responsibility of this module. (There will also be support for more dynamic management of kernel objects, but this will be handled later).

Static kernel objects in Zephyr are declared as C top-level variables (where the keyword static means something different). It is the responsibility of the calling code to initialize these items, make sure they are only initialized once, and to ensure that sharing of the object is handled properly. All of these are concerns we can handle in Rust.

To handle initialization, we pair each kernel object with a single atomic value, whose zero value indicates KOBJ_UNINITIALIZED. There are a few instances of values that can be placed into uninitialized memory in a C declaration that will need to be zero initialized as a Rust static. The case of thread stacks is handled as a special case, where the initialization tracking is kept separate so that the stack can still be placed in initialized memory.

This state goes through two more values as the item is initialized, one indicating the initialization is happening, and another indicating it has finished.

For each kernel object, there will be two types. One, having a name of the form StaticThing, and the other having the form Thing. The StaticThing will be used in a static declaration. There is a kobj_define! macro that matches declarations of these values and adds the necessary linker declarations to place these in the correct linker sections. This is the equivalent of the set of macros in C, such as K_SEM_DEFINE.

This StaticThing will have a single method init_once which accepts a single argument of a type defined by the object. For most objects, it will just be an empty tuple (), but it can be whatever initializer is needed for that type by Zephyr. Semaphores, for example, take the initial value and the limit. Threads take as an initializer the stack to be used.

This init_once will initialize the Zephyr object and return the Thing item that will have the methods on it to use the object. Attributes such as Sync, and Clone will be defined appropriately so as to match the semantics of the underlying Zephyr kernel object. Generally this Thing type will simply be a container for a direct pointer, and thus using and storing these will have the same characteristics as it would from C.

Rust has numerous strict rules about mutable references, namely that it is not safe to have more than one mutable reference. The language does allow multiple *mut ktype references, and their safety depends on the semantics of what is pointed to. In the case of Zephyr, some of these are intentionally thread safe (for example, things like k_sem which have the purpose of synchronizing between threads). Others are not, and that is mirrored in Rust by whether or not Clone and/or Sync are implemented. Please see the documentation of individual entities for details for that object.

In general, methods on Thing will require &mut self if there is any state to manage. Those that are built around synchronization primitives, however, will generally use &self. In general, objects that implement Clone will use &self because there would be no benefit to mutable self when the object could be cloned.

Structs§

Enums§

  • Objects that can be fixed or allocated.

Constants§

  • A state indicating a kernel object that has completed initialization. This also means that the take has been called. And shouldn’t be allowed additional times.
  • A state indicating a kernel object that is being initialized.
  • A state indicating an uninitialized kernel object.

Traits§

  • Define the Wrapping of a kernel object.