Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Devicetree and device driver access in Rust #76199

Closed
wants to merge 35 commits into from

Conversation

mjaun
Copy link
Contributor

@mjaun mjaun commented Jul 23, 2024

This experiment is based on the PR #75904 and demonstrates how devicetree information could be utilized in Rust which allows the creation of device driver bindings in Rust.

During the Zephyr build, the gen_defines.py script which generates the C header file containing the devicetree information already stores parsed devicetree data in the file edt.pickle. This file is used by gen_dts_cmake.py to expose necessary devicetree information to CMake. In this example an additional script gen_dts_rust.py is added which uses the same pickle file to generate devicetree information for Rust. The generated code is a static structure where each node is instantiated once as a constant where DT_NODE_0 is the root node. Nodes have references to other nodes to build up the tree structure. For nodes with status "okay", a device() method is generated allowing to access the underlying device object. Using Rust macros, similar APIs as we have in C can be provided, such as GPIO_DT_SPEC_GET.

To access Zephyr kernel functionalities as well as driver APIs, low level Rust bindings are generated using Rust bindgen. A similar approach as in this project is used to gather all compiler arguments, but the wrap_static_fns feature from bindgen is used to generate wrappers for inline functions including system calls. The low level bindings are abstracted to provide proper APIs to Rust applications. These abstractions are included depending on whether the underlying drivers are enabled like in this example with #[cfg(CONFIG_GPIO)].

The example was tested on a nucleo_f411re board. There might be architecture/configuration dependent flaws which I didn't see. The resulting main function from this example looks like this:

#[no_mangle]
extern "C" fn rust_main() {
    printk!("Hello, world! {}\n", kconfig::CONFIG_BOARD);

    let gpio_pin = GpioPin::new(gpio_dt_spec_get!(dt_alias!(led0), gpios));

    gpio_pin.configure(GpioFlags::OutputActive)
        .expect("Failed to configure pin.");

    loop {
        kernel::msleep(1000);

        gpio_pin.toggle()
            .expect("Failed to toggle pin.");
    }
}

In the pictured main function in #65837 the Rust device drivers would be instantiated directly into the devicetree structure. Here it is up to the application to instantiate the device drivers and manage them properly. The challenge I saw with the proposed approach is that although we can see in the devicetree information which compatible string is matched to a certain node, it is not clear which driver API will be exposed and therefore which Rust driver binding should be instantiated.

d3zd3z and others added 30 commits July 15, 2024 12:35
Add the `CONFIG_RUST` Kconfig.  This can be set to indicate that an
application wishes to use Rust support.

Signed-off-by: David Brown <[email protected]>
Until further variants are needed, this provides a `main()` function
that simply calls into the `rust_main()` function.  This is adequate for
applications where main can be directly written in Rust.

Signed-off-by: David Brown <[email protected]>
The initial support crate for zephyr use within a Rust application.
This crate does two simple things:

- Processes the .config file generated for this build, and ensures that
  CONFIG_RUST is enabled.
- Provide a hanging Rust panic handler.

Signed-off-by: David Brown <[email protected]>
This provides the function `rust_cargo_application()` within Cmake to
give applications written in Rust a simple way to start.

This implements the basic functionality needed to build a rust
application on Arm, including target mapping, and invoking cargo during
the build.

Signed-off-by: David Brown <[email protected]>
This implements a simple hello world application in Rust.  Since there
are no bindings yet, this just directly calls `printk` from Rust code.

Signed-off-by: David Brown <[email protected]>
Indicate the new code is maintained.

Signed-off-by: David Brown <[email protected]>
Create a crate `zephyr-build` that contains the code that will run at
build time.  Move the bool kconfig handing from this.  This will make it
easier for an application crate that needs access to config entries to
do access them by:

- Adding a dependency to zephyr-build to `[build-dependencies]` in their
  Cargo.toml.
- Creating a build.rs, or adding a call to:
  zephyr_build::export_bool_kconfig() in it.

Signed-off-by: David Brown <[email protected]>
As part of the build of the `zephyr` crate, convert string and numeric
entries from the .config file of the current build into constants.  This
mimics how these are available as defines in C code.

The defines are available under `zephyr::kconfig::CONFIG_...`.

Signed-off-by: David Brown <[email protected]>
This is a bit awkward, as we don't have a clean interface between Rust
and the Zephyr C code, but copy and null-terminate the current board,
and include that in the printed message.

This demonstrates that the kconfig module generation is working.

Signed-off-by: David Brown <[email protected]>
Add initial docs for Rust language support.  Explains the support
currently present, how to install the toolchain needed, and explains
what is in the current sample.

Signed-off-by: David Brown <[email protected]>
The documentation system requires all of the samples to have unique
names, no matter what directory they are in.  Rename our sample to
`hello_rust` to avoid a conflict with the existing `hello_world` sample.

Signed-off-by: David Brown <[email protected]>
Code-blocks for shell commands are of type 'shell', to avoid this
warning.

Signed-off-by: David Brown <[email protected]>
Distinguish between 'int' and 'hex' Kconfig values.  Use `usize` for the
hex values, as these are unsigned and may have the high bit set.  But,
for 'int' values, use `isize`, and allow the constants to be negative.

Signed-off-by: David Brown <[email protected]>
This is still very experimental, mark it in the Kconfig as such.

Signed-off-by: David Brown <[email protected]>
For Arm targets, match the ABI selection defines from the
cmake/compiler/gcc/target_arm.cmake file.  What is important here is
that the floating point selection is the same between gcc and rustc.

Signed-off-by: David Brown <[email protected]>
Because of a mismatched quote, shell quoting for this block fails
sphinx.  Removing the quite causes compliance to fail due to the
mispelling.  Fix this by just removing formatting entirely from the
block.

Signed-off-by: David Brown <[email protected]>
Compliance fails if even an example refers to non-existent Kconfig
options.  Change these to real options, even though that is slightly
distracting.

Signed-off-by: David Brown <[email protected]>
The Cortex-M4 should be the thumbv7em not thumbv7m.

Signed-off-by: David Brown <[email protected]>
Make this a proper C prototype function that takes no arguments.

Signed-off-by: David Brown <[email protected]>
@mjaun
Copy link
Contributor Author

mjaun commented Jul 23, 2024

Here are the changes without the work from #75904.

@pdgendt pdgendt added the area: Rust Issues affecting Rust support in Zephyr label Jul 23, 2024
@mjaun
Copy link
Contributor Author

mjaun commented Jul 24, 2024

@d3zd3z I only now stumbled across #76074. Sorry if this causes confusion... I would still be up to exchange some thoughts.

@d3zd3z
Copy link
Collaborator

d3zd3z commented Jul 25, 2024

Here is probably as good a place as any to discuss. As far as generating the DT from python or Rust, there are advantages disadvantages of either:

  • The python version can directly read the pickle file. Rust code has to parse the zephyr.dts file (and the header).
  • The Rust code can use quote and have fairly natural Rust-syntax snippets of code.
  • The Rust code happens as part of build.rs, which I think is easier to integrate (it adds one line to build.rs instead of dozens of cmake lines).
    I do like my approach of directly modeling the DT in Rust instead of mirroring the C macros used to access the DT. I think the C macros exist due to limitations in C, and aren't needed in Rust.
    Being able to directly say zephyr::devicetree::aliases::led0::get_instance is pretty nice. It also generates a pretty clear error message when there isn't an led0 instance. I'm not sure how well the macros you've provided work with rust-analyzer, either. But, I get full completion on the generated devicetree inside of my editor.


fn main() {
let bindings = bindgen::Builder::default()
.clang_arg("-I/home/ubuntu/zephyrproject/zephyr/include")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol. I was going to try to figure out how to get the include path out of cmake. Turns out it is rather challenging to do. We do need to figure out how to do it correctly, though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did figure out how to do this, I'll push some code hopefully tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like you were looking at an old version. Here is the best solution I could come up with for this: https://github.com/mjaun/zephyr/blob/dea56463ebbf469118ed34ee5c9e343586bf27da/cmake/modules/rust.cmake#L42

@mjaun
Copy link
Contributor Author

mjaun commented Jul 26, 2024

Here is probably as good a place as any to discuss. As far as generating the DT from python or Rust, there are advantages disadvantages of either:

* The python version can directly read the pickle file. Rust code has to parse the zephyr.dts file (and the header).

* The Rust code can use `quote` and have fairly natural Rust-syntax snippets of code.

* The Rust code happens as part of build.rs, which I think is easier to integrate (it adds one line to build.rs instead of dozens of cmake lines).
  I do like my approach of directly modeling the DT in Rust instead of mirroring the C macros used to access the DT. I think the C macros exist due to limitations in C, and aren't needed in Rust.
  Being able to directly say `zephyr::devicetree::aliases::led0::get_instance` is pretty nice. It also generates a pretty clear error message when there isn't an led0 instance. I'm not sure how well the macros you've provided work with rust-analyzer, either. But, I get full completion on the generated devicetree inside of my editor.

I would separate these things:

Processing device tree for Rust in Python vs. Rust Build Script

The Python approach has no need for an additional parser as the information is already there in a parsed form (even with information about matched bindings). Even if the CMake integration needs some code, I think overall there are is less to be added.

Then there is the separation between the Zephyr build system (CMake) and the Rust build system (Cargo). I think this is mainly relevant if you want to open and develop your Rust application in a pure Rust IDE and therefore use the Cargo project.

  • With the Python approach you need access to the generated .rs file. From the Rust world perspective this is very simple to integrate. The downside is if you want different code to be generated you need to modify something in the Zephyr world.
  • With the Rust approach you need access to the pre-processed .dts and the generated .h file. You are then more free to change the generated code, but you are more vulnerable if something in the two intermediate files changes.
  • As a third option we could maybe also emit the parsed device tree in a format which is easy to parse in Rust like for example JSON. This would leave the generation of Rust code in the Rust world and could provide all additional information which the Rust approach currently doesn't have (binding information). But this would mean yet another intermediate file format.

Instantiating Rust device driver interfaces by devicetree vs. by application

I think the major difference between our approaches is where the Rust device driver interfaces get instantiated. In your approach if I do zephyr::devicetree::aliases::led0::get_instance() I get a device driver interface ready to use. With my approach I can do device_dt_get!(dt_alias!(led0)) or also zephyr::devicetree::root().aliases.led0.device() but then I get a device handle with which I need to instantiate a device driver interface myself, like in the example with GpioDriver::new(...).

While I really do like your idea because it feels more type safe (you cannot provide a device handle to the wrong driver interface), I have one main concern. My understanding of the process is as follows:

  • We have a device tree node with possible multiple compatible strings and a status
  • Given the available YML bindings, the best one is matched against the node
  • If the driver implementation matching this YML binding is enabled (typically via Kconfig), then it typically instantiates one device per matching node with status "okay"
  • When being instantiated, a driver typically provides a well defined API for its kind (for example struct uart_driver_api)
  • A C application can then use the device handle together with syscalls for this particular API (for example uart_poll_out)
  • This is the API our Rust device driver interfaces would rely on to provide a safer interface to Rust applications

My concern is: How can we know for every case which Rust device driver interface to instantiate for a particular device tree node? As far as I can see the devicetree information can tell us the compatible string of the matched binding, but it doesn't tell which API will be exposed by the instantiated driver. We could try to guess this by saying if the compatible string contains "uart" it is probably the UART API, but this doesn't seem very robust and it surely won't work for example for proprietary out-of-tree drivers providing their own API.

If we don't know how we would solve this problem I would actually feel more comfortable if it was up to the application to instantiate the Rust device driver interfaces. It is how it is done today in C and it is guaranteed to work out in every case.

Utilize Rust capabilities to provide the safest and cleanest API possible vs. sticking closer to existing C APIs

You mentioned the C macros exist due to limitations in C. I agree that Rust allows us to craft a cleaner and safer API than what we currently have in C. In the long run I guess it makes also sense to do so, but it this takes a lot of thought work to design, implement, test and document this. A possible solution could be to create a layering like this and start implementing bottom up:

  • FFI bindings: These are generated by bindgen and are used to call into Zephyr C APIs. This is the lowest layer in Rust.
  • Safe bindings: These rely on the FFI bindings and deal with any "unsafe". We already have objects, but the provided API is still close to the C API.
  • Proper Rust bindings: These rely on the safe bindings and provide a cleaner interface for the Rust applications where this is desired. RAII guards for locks could be a good example.

Copy link

This pull request has been marked as stale because it has been open (more than) 60 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 14 days. Note, that you can always re-open a closed pull request at any time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: Rust Issues affecting Rust support in Zephyr Stale
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants