Article

How we built our Embedded World Demo on Rust for QNX

Published on 9 min read

    Background

    At Embedded World 2023, Ferrous Systems had a joint booth with AdaCore, to talk about our new joint-venture: Ferrocene. Ferrocene is a qualified toolchain for building safety-critical systems in Rust.

    Florian noted that our booth was next to the Blackberry/QNX booth, and that “we should do a demo using QNX Neutrino”, the POSIX-compatible real-time operating system that’s popular with our target audience in the automotive sector. That’s pretty much all we had to go on.

    Setting up QNX

    The first step was getting access to QNX and working out what kind of graphics support it had. We had some licences available internally, so I installed the QNX Software Center, which is a desktop GUI application, and used that to install the latest QNX Software Development Platform (SDP) 7.1. Using this, I was able to build some sample applications and look at the qnxscreen API for drawing on the screen.

    Next I needed some hardware. I called in a favour, and the next day I had two 8 GiB Raspberry Pi 4 units on my desk. We also had a Renesas R-Car H3 development kit, but that was with my colleague in Berlin. The Renesas hardware is typically what you might build an automotive dash-cluster display with, but apparently QNX had a Board Support Package for the Pi 4, and it should all be portable, right?

    Next up we needed Rust to actually compile applications for QNX. We were looking at doing a Rust Standard Library port for QNX, but then Blackberry and Elektrobit published a PR adding the aarch64-unknown-nto-qnx710 target with full Standard Library support. This was easy enough to merge over into our Ferrocene tree and compile from source, and everything worked perfectly.

    Designing the demo

    Now, we have an OS and a toolchain, but what do we want to the demo to do? I made some notes and had a think over a cup of tea, as is best in these situations. Eventually I decided I wanted to do a “retro synth-wave 1980’s style car dashboard”. Because why not? We discussed things internally, and I reached out to ura.design, to see if they could help with the artwork. We exchanged some sketches and I left them to it, working on the idea of five gauges:

    • A ‘speedometer’ reading in frames-per-second
    • Four bar-graph gauges, reading:
      • Performance
      • Safety
      • Satisfaction
      • Productivity

    Pretty important metrics that all car dashboards should be telling you about, I feel.

    Drawing with QNX Screen

    Parallel to this, I had to think about the the mechanics of making the demo work. I had written a simple C application which used the qnxscreen API and I was moving a Ferris-the-crab logo around the screen. But I really wanted to do this in Rust. My first step was to use bindgen to convert the C header file into a Rust library crate (called qnxscreen-sys), so I could call the C functions from my Rust program. There was some weirdness around how QNX uses C enums to define constants, yet expects u32 arguments to its functions. C is fine with this, but bindgen converted the constants to i32 values and Rust was requiring a lot of manual conversions from one type to the other. Ferrous Systems are currently contributing to bindgen project as part of our sponsored open-source work, so I had plenty of expert advice on-hand! In the end I decided to leave bindgen doing what it wanted to do, and fixed the type conversion in the next layer of software.

    The raw C API, albeit converted to Rust, is unsafe. That is, it uses a lot of raw pointers and while Rust lets you do that (we are systems programmers!) there’s a lot less friction if you encapsulate that unsafety into small Rust types which have a safe API. So, I started out writing qnxscreen - a medium-level Rust API for drawing on the screen using QNX.

    The C API looks like this:

    screen_context_t screen_ctx = 0;
    if (screen_create_context(&screen_ctx, SCREEN_WINDOW_MANAGER_CONTEXT) != 0) {
            perror("screen_create_context");
            return EXIT_FAILURE;
    }
    
    screen_window_t screen_window = 0;
    if (screen_create_window(&screen_window, screen_ctx) != 0) {
        perror("screen_create_window");
        return EXIT_FAILURE;
    }
    
    int format = SCREEN_FORMAT_RGBA8888;
    if (screen_set_window_property_iv(screen_window, SCREEN_PROPERTY_FORMAT, &format) != 0) {
        perror("set SCREEN_PROPERTY_FORMAT");
        return EXIT_FAILURE;
    }
    

    The C functions generally return a non-zero value on error (and also set errno). We have a screen_context (which is just a pointer to some opaque struct _screen_context that we don’t know the details of), and using the context we can create a window. There are then APIs to get window properties, including the window buffers. The buffers are then the thing you can draw on to. There’s also event handling for mouse and keyboard events and that kind of thing. Pretty comprehensive, but quite C oriented.

    My first pass was to work through the C demo application I had written, and translate it basically line by line into the kind of Rust code I would like to use. I then went and implemented enough API in my qnxscreen crate to make that bit of the application work. I then moved through the whole application until it too was showing a Ferris-the-crab moving around the screen.

    My application code looked something like this:

    let ctx = qnxscreen::Context::new(qnxscreen::ContextType::WINDOW_MANAGER)?;
    let window = ctx.create_window()?;
    window.set_screen_format(qnxscreen::FormatType::RGBA8888)?;
    

    Isn’t the Rust ? handling so much nicer than manually checking C return codes (or wrapping that C handling up into a pre-processor macro)?

    To make this work, I created types which looked like this:

    /// Represents a QNX `screen_context` object.
    #[derive(Debug)]
    pub struct Context(std::ptr::NonNull<sys::_screen_context>);
    
    impl Context {
        /// Create a new Screen Application Context.
        pub fn new(context_type: ContextType) -> Result<Self, Error> {
            let mut ptr = std::ptr::null_mut();
            let rc = unsafe {
                sys::screen_create_context(&mut ptr, context_type.bits())
            };
            map_error(rc, || {
                let Some(nn_ptr) = std::ptr::NonNull::new(ptr) else {
                    return Err(Error::NullPointer);
                };
                Ok(Self(nn_ptr))
            })
        }
    }
        
    /// Represents a QNX `screen_window` object.
    #[derive(Debug)]
    pub struct Window<'ctx> {
        ptr: std::ptr::NonNull<sys::_screen_window>,
        owned: bool,
        _phantom: PhantomData<&'ctx Context>,
    }
    

    All the ugly checking of the C errors codes we removed from our application had to go somewhere - and it’s here in the mid-level Rust wrapper. The sys prefix refers to the crate I built with bindgen, and so is basically where I interact with the C types and the C API. The PhantomData and extra lifetime annotations allows the Rust compiler to ensure that the Window I create never outlives the Context it was created from. Likewise the Buffer objects I create from a Window cannot outlive the Window they were created from. This adds a nice layer of safety over the top of the C API. QNX is pretty good at tracking the invalid memory reads if you get this wrong, but it’s nicer to have a compile error than a runtime SEGMENTATION FAULT!

    Adding the Slint UI framework

    Moving windows around the screen is all well and good, but it felt like there was quite a lot of work required to try and drive all the fancy graphics Ura.design were going to send back. Looking at the state-of-the-art of Rust based UI design, the natural choice seemed to be Slint, and so I reached out to the team.

    The Slint toolkit supports a number of backends, including a software renderer that can run on a simple no_std microcontroller, but we wanted to push a 1920x1080 picture into an HDMI screen at around 60 fps, so some kind of acceleration was required. Talking to the Slint team, I found that they would be happy to help with an Embedded World demo (it turns out they had about six already!) and that they could probably do something to adapt their EGL backend to talk to the QNX EGL implementation. That’s the whole point of an interface like EGL right, cross-platform portability?

    Well it turns out, modulo some issues with the QNX port for the Renesas R-Car H3 only supporting EGL 1.4, it was pretty straightforward. I set up an application which created a window and obtained some buffers for it. The Slint team then coded up something to take that buffer and the QNX EGL library and attach that to their EGL backend for Slint. Within only a few days, we had some logos spinning around the screen!

    If you haven’t used Slint, it uses a cross-platform declarative language which looks like this:

    import { AboutSlint } from "std-widgets.slint";
    
    export component App inherits Window {
        background: @linear-gradient(20deg, #0000ff 0%, #062b06 100%);
        Image {
            source: @image-url("./ferris.png");
            width: 200px;
            x: parent.width / 2 + parent.width / 4 * sin(animation-tick() / 10s * 360deg);
            y: parent.height / 2 + parent.height / 4 * cos(animation-tick() / 10s * 360deg);
        }
        AboutSlint {
            width: 200px;
            x: parent.width / 2 + parent.width / 4 * sin((animation-tick() + 1.2s) / 8s * 360deg);
            y: parent.height / 2 + parent.height / 4 * cos((animation-tick() + 1s) / 8s * 360deg);
        }
    }
    

    That’s all you need for two logos to spin around the screen continouously. At compile time the syntax is compiled and converted into Rust code (or C++ code if you prefer), which you can drive from your application to handle the “business logic”.

    Finishing up the demo

    All that was then required was to build a Slint model which used all the graphics from Ura.design and laid them out in the correct place. I then wrote some application logic to drive the application in one of two modes:

    • Credits Mode, which scrolled past the logos of the tools and technologies we used to build the demo
    • Trash Talk Mode, which mentioned various programming languages and then rated them on our four metrics
      • Performance
      • Safety
      • Satisfaction
      • Productivity

    Throughout the demo, we recorded the number of frames-per-second the demo was actually running and displayed that on the ‘speedometer’, to show that the demo was in fact running live on real hardware.

    And that was it! 46 commits to the demo repository for the car-dashboard application was all we needed, and that was over the space of two weeks. The QNX API wrappers took another couple of weeks before that.

    Embedded World

    On arrival at Embedded World there was a small technical hitch whilst I forgot that the Renesas R-Car H3 devkit does not automatically boot when power is applied (there’s a tiny power button you have to press), but otherwise both the Renesas board and the Raspberry Pi 4 just worked flawlessly. The application never crashed once, after 10 hours continuous operation - because why would it? Rust doesn’t let you have memory leaks or data races, and we were confident we had made good quality safe wrappers around a well-tested production-ready C API.

    Here’s a video of the demo in action in the background as I talk to Embedded Computing Design about Embedded Rust.

    There were quite a lot of Rust-related things going on at the Embedded World this year — you can find an overview of the Embedded World 2023 from the rustacean's point of view in our blog.

    If you’re interested in Ferrocene, our qualified toolchain for safety-critical Rust applications, or are interested in using Rust in any kind of application, why not get in touch and see how we can help?