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

Rust Support in Zephyr #65837

Open
d3zd3z opened this issue Nov 27, 2023 · 37 comments
Open

Rust Support in Zephyr #65837

d3zd3z opened this issue Nov 27, 2023 · 37 comments
Assignees
Labels
area: Language Bindings Language bindings, i.e. Rust, Python, Javascript, etc. RFC Request For Comments: want input from the community

Comments

@d3zd3z
Copy link
Collaborator

d3zd3z commented Nov 27, 2023

Introduction

Various parties have expressed interest in having "Rust Support" in Zephyr. This can mean different things, such as writing applications in Rust, to writing Zephyr drivers in Rust. This RFC attempts to define a specific type of support for Rust within Zephyr.

Problem description

The Rust programming language is a relatively modern programming language that attempts to bring together several features that would be appropriate for the type of embedded development Zephyr is commonly used for. Among these are:

  • Strong typing. The rust typing system allows many common programming and especially security issues to be found at compile time.
  • A sophisticated borrow tracking system. The borrow system in Rust allows compile time tracking of references, such that, in many programs, it can be known at compile time that there are never any invalid references, dangling pointers, out of bounds accesses, etc. This can even be managed safely while using dynamic memory allocation, if that is desired.
  • "Fearless concurrency". This same borrow system can also ensure that values used in multi-threaded applications are managed safely, resulting in systems where invalid access by multiple threads is caught at compile time.
  • Zero cost abstractions. The language brings the ability to have rich abstractions that add no cost to the generated code.

This RFC proposes a way for an application developer to be able to write their application, in Rust, using idiomatic Rust code (meaning unsafe is not generally needed) on top of Zephyr, while maintaining a minimal overhead to do so.

Proposed change

This support will be added in a few places:

  • Additional modules: There will be some additional modules that provide Rust crates. These can be included using Zephyr-type methods (where they are directly brought into the tree using west). These could also be published to the general crate ecosystem, if any particular crates prove useful to the general Rust community. Primary of these crates will be a zephyr crate which will be the main crate an application uses.
  • CMake support: There will be additional CMake support so that an application merely needs to reference a CMake library in order to declare they are building a Rust application. This support will invoke the cargo command to run the Rust compile, with proper arguments for the chosen board as well as necessary paths to integrate the Rust build into the Zephyr build system.
  • DeviceTree support: In addition to the C headers that are currently generated, the dt scripts will be extended to be able to generate Rust code to represent the current device tree. This can be implemented gradually as more and more parts of the Zephyr api and device surface is exposed to Rust. Most of the implementation of this interface will be done in the additional modules (crates) above, and this generated code is merely describing the particular device layout for a given build.
  • Zephyr system call interface. Those calls into Zephyr that are system calls or function calls, depending on the configuration will have a generated Rust crate/module that implements them for the current configuration.

Detailed RFC

Overall Goal.

When completed, it should be possible to have a rust implementation of "blinky" that looks something like:

#![no_std]
#![no_main]

#[zephyr::entry]
fn main() {
    let dt = zephyr::devicetree::take().unwrap();
    let led = dt.chosen.led0;

    loop {
        led.toggle().unwrap();
        zephyr::sys::sleep(1000);
    }
}

This can be filled out as the Rust interface to Zephyr features are implemented. But, this level of initial support is probably a good starting point.

Devicetree

Currently, the device tree is effectively flattened into a set of C defines for all of the values. Although it would be possible for Rust code to access values like this, the macro name stitching that is done in C would be both rather non-rust-like, as well as hide the entire device tree from tools like IDEs. It would make the generated tree mostly undiscoverable to a developer trying to write code.

Instead, we propose generating a fairly simple structure in Rust that mirrors the device tree. This structure is modeled after the PAC/HAL structures that are used within the Zephyr Embedded project. Ultimately, there is a single struct, the DeviceTree that is generated. This could, perhaps initially, be modeled after the chosen node, with only supported nodes included. Each field of this struct would be an implementation of a structure defined in a support crate for that particular device. The end result would be similar to the structures that are generated through macros to use device tree nodes currently within C.

Driver support

To make this work, much of the support Rust code will be human written code that defines the types, and methods upon those types that implement the interface to the Zephyr drivers. For some drivers, this is fairly straightforward, and will mainly result in using a cleaner Rust namespace to invoke the Zephyr devices. Support for drivers that are implemented in C with callbacks is a more complicated issue and address further below.

CMake Support

At a minimum, the cmake files will need to be extended to support building a Rust application. It will need to invoke the additional device tree generation code, convert Kconfig options to rust features, as appropriate (details to be filled in), and provide a small cmake library so that it is easy for an application to state that it is a Rust application. The cmake code will create rules that invoke cargo with the appropriate arguments, and arrange for the compiled application library to be linked into the Zephyr application.

System call support

The kernel libraries in Zephyr will be made available through a zephyr::sys module that will be a fairly straightforward mapping from Rust to the C functions/syscalls. For some routines, such as sleep, it makes sense to just call these from ordinary Rust applications. For others, such as threading, it probably makes sense to have higher level abstractions built around this.

Asynchronous support

The general trend in the Rust embedded world is to use the async mechanism to write code that is multi-threaded and needs to block. This proposal will implement a basic executor using the Zephyr scheduling primitives. A good mechanism can then be used to associate simple callbacks in driver calls to functions that send to semaphores, for example, and the semaphore and other scheduling primitives will have support in the async code to be able to wait for them.

It would also be possible to just have a 1:1 mapping of Rust threads to zephyr threads, and the primitive calls would just block the current thread to be woken appropriately. Ideally, either of these mechanisms will be available to Rust programmers to use as appropriate for a given application.

Logging

The Rust and Zephyr logging systems have some fairly significant semantic mismatches. Fortunately, The Rust language does not explicitly define a logging mechanism, and it would be fairly easy to have some simple wrappers. Log 2 would likely need to be used, as the formatting mechanism are dissimilar between the languages, and the Rust logged messages would likely just be formatted strings.

It should also be possible to configure the system to not have the Zephyr logging mechanism, and use something like the Rust defmt logger, which greatly compresses the log stream by referencing strings by index and resolving the strings by a host tool.

Dependencies

The main concern is to make sure that any support added to Zephyr for Rust should be tested as part of CI. The primary reason is so that API and other changes that break this support will be caught early, and can be fixed.

Concerns and Unresolved Questions

One concern would be how "official" rust support is by the Zephyr project. In some sense, this is largely outside of the Zephyr tree (in modules), but as it depends on the Zephyr API, if not well supported could quickly lock a given application to a specific version of Zephyr.

Alternatives

Obviously, it is possible to continue to write code in C, or C++. However, many users and companies are finding a lot of advantages to developing in Rust, and it should be possible to add this support to Zephyr without significant impact on how C development is done.

Any efforts or desires to implement parts of Zephyr in Rust are beyond the scope of this RFC, and this kind of support would have a much larger impact.

@d3zd3z d3zd3z added the RFC Request For Comments: want input from the community label Nov 27, 2023
@d3zd3z d3zd3z self-assigned this Nov 27, 2023
@pdgendt
Copy link
Collaborator

pdgendt commented Nov 28, 2023

Just wanted to point out ongoing attempts of adding rust support in cmake.

@henrikbrixandersen
Copy link
Member

One thing to note: When/if the Rust glue code (devicetree interface, driver interface, syscall interface, etc.) for writing Zephyr applications in Rust is added, this will effectively mean that developers touching these areas in Zephyr C-code will need to also need to be knowledgeable about how to do the required changes to the Rust support code to match.

@d3zd3z
Copy link
Collaborator Author

d3zd3z commented Nov 28, 2023

Just wanted to point out ongoing attempts of adding rust support in cmake.

Thanks. I think there are a couple of different things happening here. What this RFC is proposing is fairly different than what the Linux kernel is providing, which is to allow people to write Linux kernel code using Rust. In this instance, it makes sense for the Linux build system to directly invoke rustc and manage the dependencies itself. If another proposal attempts to allow Zephyr itself to be extended with Rust code, the cmake rust support work would make a lot of sense.

For this proposal, which is to allow applications to be developed in Rust, it probably makes more sense to just use cargo directly to build the rust application. This will allow the developer to use the crate ecosystem directly (there are lots of no_std crates that don't have strong dependencies on an operating system, and would be useful for this.

An even more ideal situation for this would be that a developer could just have a directory with their application, with no cmake file at all in it, and just add a dependency on a zephyr crate. This crate would handle the Zephyr build "behind the scenes". I've decided to not start with this approach because it would require a lot of changes to the Zephyr build system, and I'm not sure it is really what we want. We aren't trying to hide Zephyr from the user, but allow them to write their app in Rust.

@d3zd3z
Copy link
Collaborator Author

d3zd3z commented Nov 28, 2023

One thing to note: When/if the Rust glue code (devicetree interface, driver interface, syscall interface, etc.) for writing Zephyr applications in Rust is added, this will effectively mean that developers touching these areas in Zephyr C-code will need to also need to be knowledgeable about how to do the required changes to the Rust support code to match.

This is indeed a challenge, and what happens will kind of depend on what level of support Rust ends up. I do feel it would be sad if the Rust support didn't keep up, and required independent effort later to make it work again after any changes. I think one thing that will help here is if the rust interfacing for things where it make sense happen with the same code in tree that generates the C code. But yes, I think how this happens will depend a lot on whether Rust support becomes popular or not.

@teburd
Copy link
Collaborator

teburd commented Nov 28, 2023

I think this is great overall, I'd note that for async you could probably pretty easily build an executor on top of rtio as it is just that. An async executor with io_uring like semantics. Ownership ideas were kept in mind when I wrote it as well.

@teburd
Copy link
Collaborator

teburd commented Nov 28, 2023

One thing to note: When/if the Rust glue code (devicetree interface, driver interface, syscall interface, etc.) for writing Zephyr applications in Rust is added, this will effectively mean that developers touching these areas in Zephyr C-code will need to also need to be knowledgeable about how to do the required changes to the Rust support code to match.

I mean this is somewhat true even for C++ today, as much as people seem to believe C++ is a superset... this just isn't true and Zephyr is using C features that aren't in C++ in some cases.

Hence the entire cpp test. A rust version of this sort of thing could be constructed.

@henrikbrixandersen
Copy link
Member

I mean this is somewhat true even for C++ today, as much as people seem to believe C++ is a superset... this just isn't true and Zephyr is using C features that aren't in C++ in some cases.

In some sense it is similar, but the major difference between C++ support and Rust support would be that C++ re-uses the C definitions and APIs (so usually changes to the C APIs/macros does not require touching any C++ code), whereas Rust support would introduce new, Rust bindings for existing C APIs, requiring developers to modify/be knowledgeable about the Rust language.

I am not saying this is a show-stopper. I am merely pointing out, that this is a new side-effect compared to existing in-tree language support.

Hence the entire cpp test. A rust version of this sort of thing could be constructed.

That would be a requirement for introducing Rust bindings in my opinion.

@daniel-thompson
Copy link
Contributor

daniel-thompson commented Dec 6, 2023

Great summary.

Only observation is that async support seems a little early within the doc structure (at least assuming that it could be implement towards the end of any project plan). Maybe I haven't thought about it enough but it seems like async support ends up being built largely using foundational pieces found elsewhere in the proposal rather than having its design guided by the syscall interfaces (put another way, is it mostly built on top of sound wrappers for the Zephyr API?).

I'm not saying it wouldn't be awesome but a lot could be done without it!

@mbolivar-ampere
Copy link
Contributor

mbolivar-ampere commented Dec 8, 2023

@d3zd3z regarding devicetree support, have you considered an approach using rust's macro system instead of a data structure?

The main thing stopping us from doing a devicetree data structure in C was the ROM overhead.

Edit: example: the qemu_cortex_m3's devicetree, if converted to a DTB, is 3.4K. That is larger than the entire samples/basic/minimal binary with multithreading and timers disabled, and it's over 40% of the size of the minimal binary with multithreading and preemption enabled. It's not a trivial increase in the minimum size of a binary, and that is a very simple board -- the DTB for larger boards is in the 15K range. And that's just the data structure itself, not the tree walking code.

@Ayush1325
Copy link
Member

@d3zd3z Great proposal. I am busy with some other stuff currently but I will try to help whenever possible.

@cfriedt
Copy link
Member

cfriedt commented Dec 11, 2023

One "nice to have" thing would be ZTest integration.

Is it possible to make compiled Rust test code generate compatible iterable sections?

There are probably many different ways of running tests in Rust, but ZTest integration would be nice.

What do you think @yperess ?

@cfriedt
Copy link
Member

cfriedt commented Dec 11, 2023

I think @d3zd3z did a good job of highlighting the most important targets, also from Meta's perspective.

My original feature list (approximately)

  • Rust .a libraries / CMake (which seems to be pretty simple with our recent Discord chat)
  • Rust application integration (i.e. main() is in Rust)
  • FFI bindings to call C/C++ from Rust
  • Device drivers and Kernel services in Rust
  • System calls, logging, devicetree, etc
  • Maybe something to generate zephyr-specific "inlines" for Rust
  • Thread / channel support

ZTest support would be nice..

It would be nice to consider how to do these things in a space-optimized way so that we can use Rust on small-ish devices.

It would be nice to minimize the mental leap required to go from normal Rust (or the embedded one) to Zephyr's version of Rust (or rather for Zephyr to fill in certain standard routines).

@mcscholtz
Copy link
Contributor

This is a wonderful suggestion. We are currently working on a product that is based on Zephyr and rust, most of the application is in rust.

It has not been easy. What we ended up doing was manually wrapping only what we need in our own rust bindings which while not ideal works and is easy to understand.

We currently have a rust main function and build the rust code as a static library. Threads are defined in C and a rust macro is used on the rust side to connect with them which is a bit messy. Having the dynamic stack allocation should help a lot with this.

Some feedback of our biggest pain points / suggestions:

  1. Device/Driver binding. This is the number one issue as mentioned by @d3zd3z, dealing with C defines and macros from rust is a bit of a show stopper. So there needs to be a way to access devicetree / devices from rust.
  2. The zephyr HAL/bindings as a crate that maps to the zephyr C HAL/bindings, even if it is just the unsafe raw bindings. Having an official rust interface would allow the rust community to contribute.

Other things would be nice, like threads or logging as well as synchronization primitives. However, those can be dealt with in the current state even if it is inconvenient. Interfacing with the devicetree is a much bigger issue and is much messier to work with from rust.

@kgugala
Copy link
Collaborator

kgugala commented Dec 12, 2023

I believe starting out from something like @cfriedt's wishlist above would be very reasonable.

Starting with user space Rust support a.k.a "main() is in rust" sounds like a good option. This way the whole integration will initially stay out of the main build flow, so we'll not cause any problems to other maintainers before the Rust support is stable.

To support user apps like this we will need some functionalities, most importantly:

  • syscall bindings generator
  • error handling
  • memory management
  • threads handling
  • DTS bindings

I assume the app would be built as no_std so we'd need to provide (generate on the fly perhaps) bindings to Zephyr's kernel std handlers. All the generators would have to be integrated either with Zephyr's cmake build flow, or as a custom rust create build (whatever is more convenient for the rest of the system).

The Rust Zephyr application could be easily compiled as a library and simply linked with the kernel.

When it comes to devicetrees, I suppose we'd have to introduce an additional dts parsing tool generating a set of Rust static lookup tables using sth like e.g https://crates.io/crates/phf from the dts. I suppose we don't want to include the whole textual dts in the binary as it would blow up the memory footprint of the app.

For Ztest integration we could have a crate which will collect all the functions decorated with e.g. #[ztest] and generate a C code defining a testsuite and registering testsfunctions. We'll need to, of course, generate C/Rust bindings so we can call Rust tests from C. To be able to run the tests we also need base support for Rust applications in Zephyr.

@d3zd3z
Copy link
Collaborator Author

d3zd3z commented Dec 12, 2023

@d3zd3z regarding devicetree support, have you considered an approach using rust's macro system instead of a data structure?

The main thing stopping us from doing a devicetree data structure in C was the ROM overhead.

Edit: example: the qemu_cortex_m3's devicetree, if converted to a DTB, is 3.4K. That is larger than the entire samples/basic/minimal binary with multithreading and timers disabled, and it's over 40% of the size of the minimal binary with multithreading and preemption enabled. It's not a trivial increase in the minimum size of a binary, and that is a very simple board -- the DTB for larger boards is in the 15K range. And that's just the data structure itself, not the tree walking code.

This proposal doesn't build a data structure for the tree. It builds data structures for the device nodes, and, for the most part, these will be empty things, and take no space.

I'm thinking it will be best to fill the DT out as a module tree in rust with consts throughout. It will take no space.

@therealprof
Copy link
Contributor

In some sense it is similar, but the major difference between C++ support and Rust support would be that C++ re-uses the C definitions and APIs (so usually changes to the C APIs/macros does not require touching any C++ code), whereas Rust support would introduce new, Rust bindings for existing C APIs, requiring developers to modify/be knowledgeable about the Rust language.

Rust has C/C++ compatibility in mind and Rust bindgen can automatically create "unsafe" wrappers to almost any C code, which is exploited by thousands of "-sys" crates doing just that. Not sure how the Linux kernel deals with that but I'm pretty sure they have some clever ideas up their sleeves to not make Joe Linux Developer having to worry about Rust.

@d3zd3z
Copy link
Collaborator Author

d3zd3z commented Dec 13, 2023

In some sense it is similar, but the major difference between C++ support and Rust support would be that C++ re-uses the C definitions and APIs (so usually changes to the C APIs/macros does not require touching any C++ code), whereas Rust support would introduce new, Rust bindings for existing C APIs, requiring developers to modify/be knowledgeable about the Rust language.

Rust has C/C++ compatibility in mind and Rust bindgen can automatically create "unsafe" wrappers to almost any C code, which is exploited by thousands of "-sys" crates doing just that. Not sure how the Linux kernel deals with that but I'm pretty sure they have some clever ideas up their sleeves to not make Joe Linux Developer having to worry about Rust.

Although the bindgen might be useful for this, I think it really isn't all that helpful, mainly because much of the key things we want to bind to are defined in generated files, based on how userspace is configured. In the syscall configuration, these don't even generate C function that can be called. It might make sense to generate the rust bindings in the same place as the C bindings.

As far as drivers and such, I don't really think unsafe bindings are all that useful, except possibly, as you say, in some kind of -sys crate that would then have plainly usable crates available to provide a nice driver interface.

@adityashah1212
Copy link

adityashah1212 commented Dec 24, 2023

My two cents here. Note that I haven't worked with Zephyr but intend to use it in near future. I would want to have Rust support, since I intend to have Rust as the primary language for the application.

  1. We should have DeviceTree add syscalls be generated using the same script that currently generates for C. This is due to two reasons
    1. The generated code is zero sized. Rust Zero sized types are truly zero sized and don't take any space in memory
    2. We can avoid the whole FFI business and corresponding abstraction cost
  2. I don't think having code exposed through FFI is a great idea, especially to application code, since that means we venture into unsafe domain and that basically breaks the whole reason to support Rust. Anything that goes through FFI should be wrapped with proper API's that guarantee safety, just like standard library does with libc. Of course this rules out possibility of exposing bindgen results directly. Plus bindgen isn't great at handling function like macros. Of course, token pasting in macros is completely out of question
  3. I believe async support can be a lower priority, since to me, it feels like a nice to have than being a show stopper. I would much rather prefer support for logging, error handling (Rust style) and driver implementation support in Rust than having async. async anyway doesn't exactly honor priority. It is just lazy evaluation and can have issues with time guarantees depending on implementation. It is great for either extremely small applications, where execution times are very small or extremely large servers where time guarantees are not a requirement.
  4. Would love to see defmt in rust ecosystem for logging. Though this might be a bit tough.
  5. I love the idea of having zephyr as a crate which can be integrated with cargo. Since this opens a lot of tools and libraries that are available in Rust ecosystem
  6. As for the concern related to developers needing to understand Rust in addition with C; I think it is a bitter pill that needs to be swallowed at some point, since Rust does offer some compelling reasons to support it.

There is one additional thing, if and where possible, we should keep the public API's close to standard library of Rust for obvious reasons

@mcscholtz
Copy link
Contributor

Came across rustix which is a rust wrapper for POSIX-like API's. Seems the author was trying to upstream it so that std can be used on platforms that rustix support. However, this does not look to have materialized, see here.

However it looks like rustc depends on rustix already so it is possible that in the future it would make it easier to add std support to zephyr if we could add rustix support for supported posix functions.

This is obviously something that will take a lot of work, and might not be possible/realistic. However, since zephyr is moving in the direction of increased posix support, I thought it is important to raise the point. That and I think a long term goal of eventually getting std support would be fantastic, even if it is pretty far fetched right now.

@cfriedt
Copy link
Member

cfriedt commented Jan 3, 2024

Came across rustix which is a rust wrapper for POSIX-like API's. Seems the author was trying to upstream it so that std can be used on platforms that rustix support. However, this does not look to have materialized, see here.

Interesting... so std Rust does make libc calls under the hood? I was under the impression that was not the case. Maybe it's the no-std part of Rust that sidesteps ISO C / POSIX? Sigh... I wish I had the time to dive deep.

However it looks like rustc depends on rustix already so it is possible that in the future it would make it easier to add std support to zephyr if we could add rustix support for supported posix functions.

This is obviously something that will take a lot of work, and might not be possible/realistic. However, since zephyr is moving in the direction of increased posix support, I thought it is important to raise the point. That and I think a long term goal of eventually getting std support would be fantastic, even if it is pretty far fetched right now.

I like it, but at the same time, there are a lot of people would would probably want to assassinate me if I suggested we layer Rust on top of POSIX in Zephyr 😅 🙃 . That's really why it's there though - it's a portability layer. Helps to get the ball rolling, but does not über-optimize any implementation.

@cfriedt
Copy link
Member

cfriedt commented Jan 8, 2024

We currently have a rust main function and build the rust code as a static library. Threads are defined in C and a rust macro is used on the rust side to connect with them which is a bit messy. Having the dynamic stack allocation should help a lot with this.

Ah nifty - I wasn't sure if that might be of interest for Rust, but it's good to know that it could be.

Async Rust might be better for memory-constrained systems - effectively using coroutines instead of threads to save on stack memory / multiple thread stacks. But of course, the implication is that any calls made by the Async code need to themselves be Async or should be minimal latency, so it requires careful attention to FW design.

It would be interesting to compare the performance of two similar memory un-constrained Rust systems - one using threads and the other using coroutines.

@teburd - were you able to get any metrics like that with the Async implementation?

@teburd
Copy link
Collaborator

teburd commented Jan 8, 2024

We currently have a rust main function and build the rust code as a static library. Threads are defined in C and a rust macro is used on the rust side to connect with them which is a bit messy. Having the dynamic stack allocation should help a lot with this.

Ah nifty - I wasn't sure if that might be of interest for Rust, but it's good to know that it could be.

Async Rust might be better for memory-constrained systems - effectively using coroutines instead of threads to save on stack memory / multiple thread stacks. But of course, the implication is that any calls made by the Async code need to themselves be Async or should be minimal latency, so it requires careful attention to FW design.

It would be interesting to compare the performance of two similar memory un-constrained Rust systems - one using threads and the other using coroutines.

@teburd - were you able to get any metrics like that with the Async implementation?

Memory usage of Rust async is about optimal given the storage size of the task is perfectly matched to its state. The downside as you noted is the cooperative task scheduling aspect. That's pretty true of any async'y thing (epoll/io_uring/etc). You can see that in play with a few of the rust async schedulers, some even on bare metal now, so no need to replicate it.

There's io_uring libraries for rust async already so rtio (io_uring like API) could probably have something similar. Something has to manage rust tasks (future chains) though on top of that. So less optimal than the pure rust solution or pure C solution.

@Ayush1325
Copy link
Member

Interesting... so std Rust does make libc calls under the hood? I was under the impression that was not the case. Maybe it's the no-std part of Rust that sidesteps ISO C / POSIX? Sigh... I wish I had the time to dive deep.

It is not exactly that std Rust depends on libc. Rather std uses libc in most platforms since it is usually the most stable way to interact with OS.
However, this does not mean libc is needed for std. A good example is UEFI which has basic Rust std support (my GSoC 2022 project rust-lang/rust#105861) and directly uses UEFI protocols.

@teburd
Copy link
Collaborator

teburd commented Jan 10, 2024

esp-idf-hal also provides a std on top of freertos + esp-idf for xtensa/riscv, so its clearly possible, I don't think any libc/posix is needed in that case

Enabling std would allow using things like serde and the std thread/synchronization modules which are quite nice.

@mcscholtz
Copy link
Contributor

I hope I'm not stating the obvious but are we all aware of the work that has been done here: https://github.com/tylerwhall/zephyr-rust ?

Perhaps it is not doing things the ideal way, and it is still missing some things but already have minimal std support (Uses it's own rust fork). Could be something to look at for inspiration at the very least.

@d3zd3z
Copy link
Collaborator Author

d3zd3z commented Jan 26, 2024

I hope I'm not stating the obvious but are we all aware of the work that has been done here: https://github.com/tylerwhall/zephyr-rust ?

Perhaps it is not doing things the ideal way, and it is still missing some things but already have minimal std support (Uses it's own rust fork). Could be something to look at for inspiration at the very least.

Yes, I've definitely looked at that work. I think 'std' is probably not the best approach, mostly for the reasons seen, that it ties it tightly to the specific toolchains. Maybe that is something to consider in the further future. But, with alloc as a separate crate, a lot of what people need can be found there. But, there is definitely some good work to look into for other aspects.

@JohnCarterGonzalez
Copy link

There is @cfriedt list of early features. What would a roadmap or development timeline of something like be? I ask as a novice embedded programmer but one who is excited about the possibility of Zephyr having Rust support.

@cfriedt
Copy link
Member

cfriedt commented Feb 3, 2024

This is going to be loads of fun 😁

@JohnCarterGonzalez
Copy link

I know there is still discussion on how this will be made possible. As a early-career developer, are there references to how this kind of support is achieved? An example would be, how did C++ support come online? What part of Zephyr or systems programming could/should I be studying to learn how to contribute? I appreciate any feedback and helping me out in growing as a software engineer.

@mcscholtz
Copy link
Contributor

I have added some very basic bindings and a demo here

There are basic bindings for zephyr objects to rust std-like objects:

  • Mutex
  • Threads (just creating and joining)
  • Simple allocator

Just kernel space for now but all the calls go through syscall bindings instead of linking directly to the z_impl_* functions so userspace can be supported later.

The Mutex works like the std mutex, creating a scoped guard.

The thread takes a FnOnce () -> T closure, this requires dynamic thread stacks and allocation. This allows spawning threads like how std rust does it.

This is a very silly demo that just spawns some threads and demonstrates the use of the mutex. I have tested it on a nrf5340 devkit I have here.

I know that there are a lot of stuff missing here, but is this roughly the direction of how the community envision bindings for rust? That is create an std-like library (where there are equivalents, mutex, thread, etc..) implemented over hand written syscall wrappers

@adoerr
Copy link

adoerr commented Mar 29, 2024

That is create an std-like library (where there are equivalents, mutex, thread, etc..) implemented over hand written syscall wrappers

I fear there will be an eternal discussion as to wether the goal for Rust on Zephyr should be to enable std or wether it should strictly remain no_std. Obviously, even for no_std having an equivalent for some of the std modules, like std::sync does make a lot of sense. In the end, I guess, a case could be made for both directions.

What is needed in either case, however, is tooling / support in order to interact with the Devicetree and the configuration / build system. Based on such a set of tools there should be a build target which will generate the low-level (FFI) Rust bindings, taking into account a set of configuration items (support for user space etc.)

With that level of support we have at least addressed some of the major pain points regarding Rust support. In addition, being able to generate those low-level Rust bindings it will be much easier for developers familiar with (embedded) Rust but not so much with the intricacies of the Zephyr build and configuration system, to start building on top.

Perhaps, as a community, we should focus on this level of support first, so that the RFC moves forward?

In a sense, we would kind of mimic the approach seen within the bare-metal embedded Rust community. Namely, having a set of (layered) crates starting with architecture support, then a runtime crate on top etc.

@mcscholtz
Copy link
Contributor

What is needed in either case, however, is tooling / support in order to interact with the Devicetree and the configuration / build system. Based on such a set of tools there should be a build target which will generate the low-level (FFI) Rust bindings, taking into account a set of configuration items (support for user space etc.)

I 100% agree that is the more difficult problem that needs to be addressed and one I am not sure what a good approach would be. Not sure how you would link something like Kconfigs to rust features for example.

Also agree about the device tree, bindings can be made for system calls like this:

__syscall bool device_is_ready(const struct device *dev);
__syscall const struct device *device_get_binding(const char *name);

Similar bindings can be made for drivers.

However, for anything more involved like parsing the devicetree into some sort of compile time rust representation there will need to be some hefty work done on the build scripts. Which will be better but require a lot of work compared to the above.

Rust already addresses some of the reasons for using userspace (not all). For userspace support more work is needed, I have done this in the past but having to decorate objects with linker sections is cumbersome, see below. Not to mention all of the memory partitions are created via C macros. This can be alleviated by some rust macros, probably.

#[link_section = "data_smem_rust_app_heap_partition_data"]

There are also other systems where the shear amount of C macros used makes it easier in some cases to use the core OS primitive bindings to re-implement the feature in Rust. Not sure how to reconcile the Kconfig options with the behavior of the rust in such a case.

@Ayush1325
Copy link
Member

Ayush1325 commented Apr 11, 2024

I 100% agree that is the more difficult problem that needs to be addressed and one I am not sure what a good approach would be. Not sure how you would link something like Kconfigs to rust features for example.

The Linux kernel already does this, so we have a reference implementation for it.

Also agree about the device tree, bindings can be made for system calls like this:

__syscall bool device_is_ready(const struct device *dev);
__syscall const struct device *device_get_binding(const char *name);

Similar bindings can be made for drivers.

However, for anything more involved like parsing the devicetree into some sort of compile time rust representation there will need to be some hefty work done on the build scripts. Which will be better but require a lot of work compared to the above.

Rust already addresses some of the reasons for using userspace (not all). For userspace support more work is needed, I have done this in the past but having to decorate objects with linker sections is cumbersome, see below. Not to mention all of the memory partitions are created via C macros. This can be alleviated by some rust macros, probably.

#[link_section = "data_smem_rust_app_heap_partition_data"]

There are also other systems where the shear amount of C macros used makes it easier in some cases to use the core OS primitive bindings to re-implement the feature in Rust. Not sure how to reconcile the Kconfig options with the behavior of the rust in such a case.

I think it might be good to follow Linux kernel example in how to integrate Rust support. The initial Rust support did not need to have devicetree or driver support (still mostly doesn't). Instead, it would be best to start with getting the plumbing to compile hello world application and allow writing stuff (crypto algorithm, etc) which require minimal interaction with Zephyr stuff.

The support to interact with other Zephyr stuff can come later, once the plumbing for Rust and bindgen is all there.

I personally do not think there is much benefit to having std support, at least not for quite a while. If I had to compare std to something, it would be POSIX (and not just c std). While it might be nice to have, most embedded developers would require the ability to use Zephyr APIs directly once they hit the limits of what the std API provides.

The biggest reason to use Rust, at least for me, is the compiler (aka borrow checker), not the std.

@d3zd3z
Copy link
Collaborator Author

d3zd3z commented Jul 15, 2024

I personally do not think there is much benefit to having std support, at least not for quite a while. If I had to compare std to something, it would be POSIX (and not just c std). While it might be nice to have, most embedded developers would require the ability to use Zephyr APIs directly once they hit the limits of what the std API provides.

I agree on std. I think earlier efforts focused more on std, because things like allocation and collections were tied to that. Now that alloc is a separate crate, I think it makes a lot more sense to support alloc, and then provide interfaces as appropriate for Zephyr.

@mjaun
Copy link
Contributor

mjaun commented Jul 23, 2024

Thank you very much @d3zd3z for driving this topic. I did some experiment based on your pull request #75904 which I would like to share. It contains a blinky example to demonstrate how devicetree information could be accessed from Rust. You can find it here: #76199

I would be interested what you guys think. If you would like me to do some other experiments or could use support on some implementation please let me know, I would be happy to contribute.

@kloenk
Copy link

kloenk commented Aug 23, 2024

I 100% agree that is the more difficult problem that needs to be addressed and one I am not sure what a good approach would be. Not sure how you would link something like Kconfigs to rust features for example.

The Linux kernel already does this, so we have a reference implementation for it.

This is build with non cargo in mind, but the response file should somehow be managable through cargo, though a bit more pain. As far as I looked into zephyr kconfig it uses the python implementation, where I don't know how to add this response file generation as added to the C version of KConfig. Happy to help to implement that though just don't wanna touch unknown python code alone (I did the KConfig support for rust response files in the linux tree quite some time ago)

@kloenk
Copy link

kloenk commented Aug 24, 2024

Created a basic rustc_cfg writer. No clue how to best wire it up though, help would be appreciated.

Tried it by just stupidly searching for autoconf.h and so far it seems to be working, though I did not look at cargo at all yet, and my cmake is even worse then my python kloenk@2fe4b33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: Language Bindings Language bindings, i.e. Rust, Python, Javascript, etc. RFC Request For Comments: want input from the community
Projects
Status: No status
Development

No branches or pull requests