A problem met while trying to make a higher level abstraction for embedded-graphics

Hi everyone,

Recently I started planning to write a program that can drive a SSD1306 (a simple I2C OLED display) to display my single board computer's stats. I searched GitHub and found embedded-graphics (available on crates). It worked really well by itself, but when I'm trying to make some more abstractions based on it, I met a problem.

Because I'm using a SBC instead of a microcontroller, resource is not that limited and re-allocation is possible and acceptable. My goal is to create something like a "rendering query" so that I can separate the the part that draws the widgets from the part that do the rendering and actual displaying. In order to do that, I tried to create a vector of trait objects of the Drawable trait by doing something like this:

struct WidgetsBundle<C> {
    widget: Vec<Box<dyn Drawable<C>>>,
}

It failed with E0038 and Ferris says it is because the trait Drawable itself has a type parameter.

pub trait Drawable<C> 
where
    C: PixelColor, 
{
    fn draw<D: DrawTarget<C>>(self, display: &mut D) -> Result<(), D::Error>;
}

(The original declaration of the Drawable trait)

I tried to workaround that by implementing another trait that does not have the type parameter because in my use case there is only one certain type of display.

pub type Display = GraphicsMode<I2cInterface<I2cdev>>; // From some other libraries

pub trait Widget {
    fn draw(self, display: &mut Display);
}

But when I failed at trying to implementing Widget to Drawable, and I did not find many information about how to solve this.

I wrote a mock code for the situation I'm facing on Rust Playground:

// For my usage, I want to store data that has implemented the ForeignTrait.
// So at the beginning, I used Box<dyn ForeignTrait<Y>> and got E0038,
// because ForeignTrait<Y> has a type parameter Y.
//
// In order to solve that, I wrote my own trait(MyTrait, l. 37),
// because I'm only going to use MyType(l.22) and it had already implemented
// ForeignTrait(l. 29 and all the other required traits).
//
// Problem (either one of them):
// 1. Is it possible to create a trait object for ForeignTrait.
// 2. If 1 cannot be accomplished, is there anyway to implement MyTrait for
//    ForeignTrait.

trait WhateverTrait<Y> {
    // Just a dummy trait
}

trait YetAnotherTrait {
    // This trait does not matter
}

struct MyType;

// My type already implemented both traits
impl<Y> WhateverTrait<Y> for MyType {}
impl YetAnotherTrait for MyType {}

// This trait is from a library that I cannot alter
trait ForeignTrait<Y>
where
    Y: YetAnotherTrait, 
{
    fn foreign_function<W: WhateverTrait<Y>>(self, whatever: &mut W);
}

// Replaced WhateverTrait<W> with MyType since I'll only be using MyType.
trait MyTrait {
    fn foreign_function(self, whatever: &mut MyType);
}

// Trying to make ForeignTrait a trait object (E0038).
// use std::vec::Vec;

// struct MyGoal<Y>
// where
//     Y: YetAnotherTrait,
// {
//     inner: Vec<Box<dyn ForeignTrait<Y>>>,
// }

// My attemps to impl MyTrait for ForeignTrait
// 1st attemp (E0109, E0107).
// impl<Y, F> MyTrait for F<Y>
// where
//     Y: YetAnotherTrait,
//     F: ForeignTrait,
// {
//     fn foreign_function(self, whatever: &mut MyType) {
//         ()
//     }    
// }

// 2nd attemp (E0207).
// impl<Y, F> MyTrait for F
// where
//     Y: YetAnotherTrait,
//     F: ForeignTrait<Y>,
// {
//     fn foreign_function(self, whatever: &mut MyType) {
//         ()
//     }
// }

// 3rd attemp (E0038).
// impl<Y> MyTrait for dyn ForeignTrait<Y>
// where
//     Y: YetAnotherTrait,
// {
//     fn foreign_function(self, whatever: &mut MyType) {
//         ()
//     }
// }

fn main() {
    println!("Hello, world!");
}

(Playground)

Is there anyway to accomplish the "render query"-like abstraction I would like to make? I'm worrying about am I thinking it wrong.

Thanks everyone for reading this.

I don't have a good solution, but will say that the challenge you're running into is that Rust is not object oriented, so many OO patterns don't work well in Rust.

Yea... But I don't think this is OO either. All I want is some type of storage for all the widgets with the Drawable trait, a vector can definitely do the job. Using a struct is simply because I may bundle more thing into it...

I heard from my friends that this is more like a higher-kinded-type thing from FP, I just don't know :rofl:

If there is no way to do that thing... I may just use some kind macro or whatever.

The problem is not that the Drawable trait is generic, it is that the draw function is generic. That means that even if you know an object implements drawable, you don't know which draw function to call for it (as each draw target needs a different function). So you can't simply abstract the draw calls behind a dyn Drawable.

This compiles, for instance:

pub trait DynDrawable<E, C>
where
    C: PixelColor,
{
    fn draw(self, display: &mut dyn DrawTarget<C, Error=E>) -> Result<(), E>;
}

pub trait PixelColor {}
pub trait DrawTarget<C: PixelColor> {
    type Error;
}

struct WidgetsBundle<E, C> {
    widget: Vec<Box<dyn DynDrawable<E, C>>>,
}

What I did here is make the draw target a trait object to remove the generic parameter on draw. Perhaps you can wrap your interface in some manner so you can do the same?

Since there's no (safe) way to make a trait object from Drawable, you might want to consider storing your widgets as closures, especially if draw takes ownership of self. That might look something like this:


struct WidgetsBundle<D: DrawTarget<C>, C: PixelColor> {
    widget_draws: Vec<Box<dyn FnOnce(&mut D) -> Result<(), D::Error>>>,
}

impl<D: DrawTarget<C>, C: PixelColor> WidgetsBundle<D, C> {
    pub fn push<W: Drawable<C> + 'static>(&mut self, widget: W) {
        self.widget_draws.push(Box::new(move |display: &mut D| widget.draw(display)))
    }
    
    pub fn draw_all(&mut self, display: &mut D) -> Result<(), D::Error> {
        for widget in self.widget_draws.drain(..) {
            (widget)(display)?
        }
        Ok(())
    }
}

Thank you very much, that works.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.