Linked Questions

Popular Questions

I'm writing some software in C that's intended to be run on different microcontrollers in different configurations. There are several types that I want to vary based on the desired configuration, but these types should be determined at compile time. It is OK for the final target to have knowledge of the specifics of the type, but all the common abstractions should have minimal knowledge of the underlying types to allow flexibility.

In C#, C++, and Rust I would use generics to perform this, but this is not an option in C.

Specific Use Case

A specific use case is to define the color mode the embedded device receives image data in and can operate in. For example, there are some cases where the device should be compiled to work with 16-bit color in RGB-565 format, while other times it may be using 24-bit RGB-888 format, as well as other types. Each of these types requires working with the color data differently in order to get well optimized rendering code, but importantly the device is able to only support a single mode throughout its finalized compiled firmware.

Options I Know about

Option 1: Using IfDefs

One option I came up with using #ifdef regions. An example in a color.h file would be

#ifdef COLOR_MODE_RGB_565

typedef uint16_t Mgpu_Color;

Mgpu_Color mgpu_color_from_rgb565(uint8_t red, uint8_t green, uint8_t blue);
void mgpu_color_get_rgb565(Mgpu_Color color, uint8_t *red, uint8_t *green, uint8_t *blue);

#endif

Mgpu_Color mgpu_color_from_rgb888(uint8_t red, uint8_t green, uint8_t blue);
void mgpu_color_get_rgb888(Mgpu_Color color, uint8_t *red, uint8_t *green, uint8_t *blue);

The intention of this is that if my target executable defines COLOR_MODE_RGB_565 then the Mgpu_Color type is a uint16_t.

I then have a color_rgb565.c file with RGB565 specific implementations of the mgpu_color_from_rgb888() and mgpu_color_get_rgb888() functions.

In my target executable that's being built in RGB565 mode, I then have target_compile_definitions(microgpu_sdl_fw PUBLIC COLOR_MODE_USE_RGB565) to tell it to use this color mode, and then add color_rgb565.c as part of the target's compile list.

This seems to work, but seems like it can get unwieldy as the number of color modes grows, especially if I want to include color mode specific color constants.

Option 2: Using Opaque Types

Another option I've come across is using an opaque type that's externally defined in a separate header file. An example of this would have the color.h as

typedef struct Mgpu_Color Mgpu_Color;

Mgpu_Color* mgpu_color_from_rgb888(uint8_t red, uint8_t green, uint8_t blue);
void mgpu_color_get_rgb888(Mgpu_Color *color, uint8_t *red, uint8_t *green, uint8_t *blue);

and then have a color_rgb565.h that looks like:

struct Mgpu_Color {
    uint16_t value;
};

Mgpu_Color* mgpu_color_from_rgb565(uint8_t red, uint8_t green, uint8_t blue);
void mgpu_color_get_rgb565(Mgpu_Color *color, uint8_t *red, uint8_t *green, uint8_t *blue);

I can then have a color_565.c that implements all four functions. This feels like it's more scalable from a file and navigation point of view, and my understanding (which could be wrong) is that the color->value indirection should be cheap since it's always pointing to the first memory address, thus color->value just gets compiled to the address of color.

That being said, this means everywhere must deal with pointers to colors instead of cleanly stack allocating them via Mgpu_Color someColor. This seems like it's going to be painful in many cases, such as deserializing commands from a remote system and returning a struct with a color value in it. This also seems like I'm going to have to malloc single colors that I need to pass around, which not only means extra free() concerns, but also I want to keep malloc to a minimum on embedded systems if possible.

Conclusion

Are there any other methods to achieve compile time differences in types? Are there any major pros or cons to either of these two options that I'm not aware of?

Edit:

A simplified example of how I would accomplish this in Rust would be

trait ColorMode {
    fn get_rgb888(&self) -> (u8, u8, u8);
}

#[derive(Clone)]
struct ColorModeRgb565 {
    value: u16,
}

impl ColorMode for ColorModeRgb565 {
    fn get_rgb888(&self) -> (u8, u8, u8) {
        // code to extract rgb from 16-bit `value` here
        // code to convert 565 values to 888 here
    }
}

impl ColorModeRgb565 {
    pub fn get_rgb565(&self) -> (u8, u8, u8) {
        // code to extract raw rgb values here
    }
}

struct FrameBuffer<T: ColorMode> {
    pixels: Vec<T>,
}

impl<T: ColorMode + Clone> FrameBuffer<T> {
    pub fn new(width: usize, height: usize, default: T) -> Self {
        FrameBuffer {
            pixels: vec![default; width * height],
        }
    }

    pub fn get_pixel(&self, x: usize, y: usize) -> T {
        pixels[(y * width) + x]
    }

    pub fn set_pixel(&mut self, x: usize, y: usize, color: T) {
        pixels[(y * width) + x] = color;
    }
}

fn main() {
    let black = ColorModeRgb565 { value: 0 };
    let mut frameBuffer = FrameBuffer::<ColorModeRgb565>::new(100, 100, black);

    let color = frameBuffer.get_pixel(1, 1);
    frameBuffer.set_pixel(2, 2, color);
}

Related Questions