Rust Embedded Graphics Backends: Choose a Generic or Enum Boundary Instead of dyn DrawTarget

When a renderer must work with two display drivers, an erased dyn DrawTarget parameter looks attractive. With embedded-graphics 0.8.2, however, that boundary is invalid even when the driver and application use the same compatible trait definition. The trait itself is not dyn compatible.

The practical default is a renderer generic over the concrete target. If the application must select among a known set of backends at runtime, place an enum and a match outside that renderer. This keeps runtime selection separate from the generic drawing operations.

Identify the actual restriction

The 0.8.2 DrawTarget declaration contains associated Color and Error types, plus generic methods including draw_iter<I> and fill_contiguous<I>. Its documentation explicitly marks the trait as not dyn compatible.

The Rust Reference’s dyn-compatibility rules exclude dispatchable methods with type parameters. Fixing Color and Error does not remove the iterator type parameter on those drawing methods. Putting the same trait behind Box also leaves that restriction intact.

This declaration is an intentionally rejected design fixture, not recommended code:

use core::convert::Infallible;
use embedded_graphics::{draw_target::DrawTarget, pixelcolor::Rgb565};

// Deliberately invalid for embedded-graphics 0.8.2.
type ErasedDisplay = dyn DrawTarget<Color = Rgb565, Error = Infallible>;

If the compiler instead reports that a concrete driver does not implement the expected trait, investigate that separate interface problem. This article assumes a coherent trait identity; changing dependency versions cannot make these generic methods disappear from this pinned API.

A rejected dyn DrawTarget boundary sits above an enum match that selects concrete backend A or B and invokes a shared generic render function while preserving errors.
Original abstraction-boundary diagram. It illustrates a proposed generic and finite-enum design, not compiler output or measured performance.

Keep one scene generic over its target

The following proposed renderer fixes the scene’s color contract while preserving the backend’s error type. It requires no heap allocation at this boundary:

use embedded_graphics::{
    draw_target::DrawTarget,
    geometry::{Point, Size},
    pixelcolor::{Rgb565, RgbColor},
    primitives::Rectangle,
};

fn render<D>(target: &mut D) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    target.fill_solid(
        &Rectangle::new(Point::new(0, 0), Size::new(8, 8)),
        Rgb565::GREEN,
    )
}

A supported concrete driver can be passed directly to this function. A framebuffer-backed test target can use the same scene when it implements the required color contract. The body remains shared source even though the compiler works with a concrete target type at each instantiation.

Keep driver initialization and any required flush operation outside the scene unless the application’s own interface deliberately includes them. DrawTarget describes drawing; its documentation notes that some drivers require a separate flush before buffered changes reach the display. Returning Ok(()) from the scene therefore does not universally mean the panel has updated.

Select a finite backend at the application boundary

If the product supports either of two driver types, an enum can retain them without constructing a dyn DrawTarget. Using the renderer above, a proposed dispatch layer is:

enum DisplayChoice<A, B> {
    First(A),
    Second(B),
}

enum RenderError<EA, EB> {
    First(EA),
    Second(EB),
}

fn render_selected<A, B>(display: &mut DisplayChoice<A, B>)
    -> Result<(), RenderError<A::Error, B::Error>>
where
    A: DrawTarget<Color = Rgb565>,
    B: DrawTarget<Color = Rgb565>,
{
    match display {
        DisplayChoice::First(a) => render(a).map_err(RenderError::First),
        DisplayChoice::Second(b) => render(b).map_err(RenderError::Second),
    }
}

Each branch passes a concrete type to the generic scene, and the error enum preserves which backend failed. Adding a backend requires extending this finite choice. It is not an open-ended plugin interface.

The two backends must still meet the scene’s color requirement. An RGB target and a monochrome target do not become interchangeable because they appear in different enum variants. Choose an explicit conversion or a differently parameterized scene when that requirement changes.

Use a narrower erased interface only when it is the real requirement

An application can design its own dyn-compatible interface with nongeneric operations, such as rendering a specific status view or accepting a fixed pixel slice. That is a new adapter contract. It must define colors, bounds, errors, ownership and how each concrete backend receives the operation.

Do not implement that adapter by casting a dyn DrawTarget into existence. Also review which optimized target operations the adapter preserves. Reducing everything to individual pixels can discard useful bulk-fill behavior; the API documentation permits drivers to override those operations. No code-size or speed advantage follows automatically from either design.

Qualify the boundary with two real target types

Pin the 0.8.2 dependency, compile the generic scene against both intended backends, and exercise both enum branches. Include a backend that returns a deliberate error so the application proves it preserves the error path. Separately test any required flush and panel update.

Keep the rejected trait-object fixture in a separate compile-fail check, not the production module. The examples here were checked against the documented signatures but were not compiled; no display test or performance measurement was performed. The outcome to establish is precise: one scene reaches each supported concrete target through a valid abstraction boundary, without pretending the underlying trait supports dynamic dispatch.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *