-
Notifications
You must be signed in to change notification settings - Fork 6.7k
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
Comments
Just wanted to point out ongoing attempts of adding rust support in cmake. |
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. |
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 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 |
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. |
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. |
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. |
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.
That would be a requirement for introducing Rust bindings in my opinion. |
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! |
@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. |
@d3zd3z Great proposal. I am busy with some other stuff currently but I will try to help whenever possible. |
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 ? |
I think @d3zd3z did a good job of highlighting the most important targets, also from Meta's perspective. My original feature list (approximately)
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). |
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:
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. |
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:
I assume the app would be built as 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. |
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. |
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. |
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.
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 |
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. |
Interesting... so
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. |
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. |
It is not exactly that |
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. |
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 |
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. |
This is going to be loads of fun 😁 |
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. |
I have added some very basic bindings and a demo here There are basic bindings for zephyr objects to rust std-like objects:
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 |
I fear there will be an eternal discussion as to wether the goal for Rust on Zephyr should be to enable 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. |
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:
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.
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. |
The Linux kernel already does this, so we have a reference implementation for it.
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 The biggest reason to use Rust, at least for me, is the compiler (aka borrow checker), not the std. |
I agree on std. I think earlier efforts focused more on std, because things like allocation and collections were tied to that. Now that |
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. |
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) |
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 |
Is there anything I can currently do? would like to use embedded-graphics but don't wanna fight with CMake so hope someone can do that with my new kconfiglib changes |
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:
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:
zephyr
crate which will be the main crate an application uses.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.Detailed RFC
Overall Goal.
When completed, it should be possible to have a rust implementation of "blinky" that looks something like:
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.
The text was updated successfully, but these errors were encountered: