Porting from C: static, references, no heap and so much usafe

TL;DR:

I'd appreciate ideas to port C code to idiomatic Rust, given a no-heap usage constraint.

Long version:

I'm looking into using Rust for a (re)implementation of a deep embedded project. Due to safety restrictions (automotive and more) I am not allowed to do any heap allocations.

In the original C code, a device tree is built from a collection of static structs that reference each other; each driver and each instance is represented by a struct; driver classes are represented by a struct with function pointers which can be implemented by specific drivers which implement these functions. Attached below is a link to this C code (simplified, as the original code makes heave use of the preprocessor to do the boilerplating)

I guess using traits and boxing would be the idiomatic way to build a tree like this in Rust; I made this work without problems, but given the no-heap constraints this solution is not usable at this time.

My efforts to make a dumb port from the C code end up in very nasty code: full of unsafes, and in the end not even compiling because of the error "mutable references are not allowed in the final value of statics".

I would appreciate any hints on how to port the original C code to idiomatic Rust, working within the same constraints of using no heap memory.

This is my original C code: http://ix.io/3ofY/c
This is a non-function dumb port to Rust: http://ix.io/3og1/rust

Thanks, and apologies for the messy nature of this question.

How about this example? The only unsafe it uses is an unsafe impl Sync, which is safe as long as your code is single-threaded. This example does not heap allocate.

use core::cell::Cell;

// GPIO Driver interface

trait DrvGpio: Send + Sync {
    fn init(&self);
    fn get(&self) -> bool;
}

// GPIO device instance

struct DevGpio {
    drv: &'static dyn DrvGpio,
    nr: i8,
}

// Generic gpio functions

impl DevGpio {
    fn init(&self) {
        self.drv.init();
    }
    
    fn get(&self) -> bool {
        self.drv.get()
    }
}


// Linux GPIO driver

struct GpioLinux {
    value: Cell<i8>,
}
unsafe impl Sync for GpioLinux {}

impl DrvGpio for GpioLinux {
    fn init(&self) {
        println!("init linux, value = {}", self.value.get());
    }
    fn get(&self) -> bool {
        let prev = self.value.get();
        self.value.set(prev + 1);
        prev % 2 == 0
    }
}


// STM32 GPIO driver

struct GpioArm {
}

impl DrvGpio for GpioArm {
    fn init(&self) {
        println!("init stm32");
    }
    fn get(&self) -> bool {
        false
    }
}


// GPIO device instances

static gpio1_data: GpioLinux = GpioLinux {
    value: Cell::new(1),
};

static gpio2_data: GpioLinux = GpioLinux {
    value: Cell::new(1),
};

static gpio3_data: GpioArm = GpioArm {
};


static gpio1: DevGpio = DevGpio {
    drv: &gpio1_data,
    nr: 32,
};
static gpio2: DevGpio = DevGpio {
    drv: &gpio2_data,
    nr: 33,
};
static gpio3: DevGpio = DevGpio {
    drv: &gpio3_data,
    nr: 14,
};


fn main() {
    gpio1.init();
    gpio2.init();
    gpio3.init();
    
    println!("{}", gpio1.get());
    println!("{}", gpio1.get());
    println!("{}", gpio1.get());
}

Note: Please don't use that paste website. It puts line numbers in my clipboard when I try to copy it.

Excellent, this exactly shows which components I was missing; thanks for that!

You coule use AtomicI8 to get rid of even that one line of unsafe if you have access to it

2 Likes

Step 1: make sure to have a comprehensive suite of software tests which will let you quickly tell when you've broken something.

Step 2: write some Rust that does exactly what the C does (i.e. construct vtables and populate static variables). This is probably going to be verbose, but not too technically difficult. You'll have to write a fair amount of unsafe but that's okay, unsafe isn't the boogeyman, it just requires you to document your assumptions. You mention working in a safety-critical environment, so this will be no different to your existing C code.

Step 3: Once you can write some ugly unsafe Rust which lets you plug into the existing system you can start developing abstractions which reduce verbosity and let you put a nice boundary around any unsafe code you may have. This will probably involve creating traits for the different driver families then some conversion routines for creating the corresponding vtable (this article I wrote a while back may be helpful here).

I'd also like to mention that the lack of heap allocations isn't a massive issue. You can still use Rust trait objects with references to existing objects (either on the stack or static variables) - there is nothing about dynamic dispatch that requires Box.

Another bit of advice is to incrementally try to port this code. Create one component using Rust then test the bejesus out of it to help you iron out the bugs in your design and give you/your team a chance to build confidence. Like most other projects, when you rewrite massive chunks of the system you'll find that it always takes longer than you expected and the new implementation is never as awesome as it was in your head.

1 Like