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§
- A kernel object represented statically in Rust code.
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.