tock / libtock-rs

Rust userland library for Tock

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How drivers API should look like

alexandruradovici opened this issue · comments

I am porting over to Tock 2.0 several drivers and I stumbled upon the following questions.

  1. How low level or high level should the drivers' API be?
  2. What about the asynchronous API?

Here are a few of my thoughts to be used as s starting point for this topic.

Definitions

Before I go into details, I'll briefly describe the terminology used.

  • low level driver API means having a thin layer on top of the system calls. Its main purpose is to transform the raw data passed back and forth between the user space and the kernel into Rust meaningful structures. (eg: PinMode::High to 1 an vice versa). This API style mimics the exact system call interface.
  • high level driver API means having another layer on top of the system call API that exposes more advanced data structures (eg: Pin, OutputPin, InputPin)

API Variants

The question is what kind of level do we want to provide in libtock 2.0? I'm presenting here four possible scenarios:

Low Level Only

Libtock will offer only the low level driver API. This means a more or less 1:1 mapping to the system call interface and transforming the basic data types into meaningful Rust structures. This kind of API is exposed by the #369 (button driver).

let value = Button::read(0).unwrap();
Buttons::enable_interrupts(0);

Using upcalls would look like:

let listener = ButtonListener(|button, state| {
    // handle event
});
Buttons::enable_interrupts(0);
share::scope(|subscribe| {
   Buttons::register_listener(&listener, subscribe);
   // yield and wait for events
});

✅ low memory footprint
✅ no heap allocation
❌ users will have to write a lot of boilerplate
❌ users will have write additional code

High Level Only

Libtock 2.0 will offer only a high level driver API, fully abstracting the actual system call interface. Such an example is provided by #367 (gpio driver). Data types are annotated for better understanding.

let pin_0 = Gpio::get_pin(0);
let _ = pin_0.map(|mut pin: Pin| {
    let output_pin = pin.make_output();
    let _ = output_pin.map(|mut pin: OutputPin| {
        pin.set();
    });
});

Using upcalls would look like (not actually implemented now):

share::scope(|subscribe| {
  let pin_0 = Gpio::get_pin(0);
  let _ = pin_0.map(|mut pin: Pin| {
      let input_pin = pin.make_input::<Pull>();
      let _ = input_pin.map(|mut pin: InputPin<Pull>| {
          pin.on_rising(subscribe, |gpio| {
             // handle event only for this pin
          })
      });
  });
});

This approach requires heap allocation to at least be able to store a Vec of pins and redirect events to each of them.

✅ easy to use, less boilerplate code
✅ users are not required to write additional code
❌ most of the API requires heap allocation
❌ larger memory footprint

Low and high level (depending on the driver)

Libtock 2.0 will provide both low and high level APIs, depending on the driver. This is how #367 (gpio driver) is implemented right now. The high level API is used for pin access (read and write) while for handling interrupts only a low level API is exposed.

Another example that comes into mind is the timer driver. If only a low level API is provided, users will not be able to set multiple alarms.

✅ small memory footprint
✔️ easier to use, less boilerplate code
✔️ users are not required to write a lot of additional code
❌ some of the API requires heap allocation

Low level + additional high level API crates libraries

Libtock 2.0 provides out of the box the low level API drivers. These drivers do not use any memory allocation and have a small footprint.

Besides these standard API, libtock 2.0 provides additional crates for high level API. For instance, libtock could provide the gpio driver and an additional pin driver crate that sits on top of gpio. The pin driver can than use memory allocation.

Simple applications can choose not to include the pin crate, thus not requiring any memory allocation and reducing their footprint. More complex applications can include the pin crate, but at a higher resources cost.

The current status

  • button (#369) is a low level API driver
  • leds (#359) is a low level API driver
  • gpio (#367) is a low and high level API driver, it exposes a high level API for pins and a low lever API for handling interrupts (the only way to avoid memory allocation - I think)

In general, it should be possible to use libtock-rs' APIs without using dynamic memory allocation. Therefore I'm opposed to the "high level only" approach.

Low level APIs can be designed by simply looking at the system call documentation, but I believe that designing a good higher-level API requires some understanding of how that API will be used. E.g. if I tried to design a high-level interface to the touch driver, I would probably create a poor API, because I'm not a user of that interface myself. Therefore, my recommendation is that all high level APIs should be designed by contributors who use that API.

Another example that comes into mind is the timer driver. If only a low level API is provided, users will not be able to set multiple alarms.

It's definitely possible to virtualize alarms without needing dynamic memory allocation -- the Tock kernel already does so. However, given that libtock-rs currently only supports local asynchrony, I'm not sure that anyone would need that style of virtualization in a libtock-rs app.

Having just written my first driver API, I think the "both" option is the closest to ideal.

I think there is an additional criterion, relevant for niche/small projects: how easy is it to write the thing? I found the console driver quite tedious to write, and the motivation I might have spent going an extra mile went into writing tests instead.

With that in mind, my personal heuristic translates into the following hierarchy:

  • avoid allocations
  • simple to use
  • easy to write
  • powerful
  • uses little resources

It favors having a slow/hungry, but friendly and maintainable driver, rather than not having one at all.

To summarize: write as high level as makes sense without allocations. If you still need allocations, create a new higher-level crate.

Is there any sample for this API?