Advice needed: trait objects with associated types/consts

In this thread, I ask for help untangling some type-level API designs that aren't working as well as I'd hoped. It's less technical question, and more advice seeking. It's also, like, really long, with lots of context about what I'm trying to do, but maybe some of y'all like reading stories, so here goes. I hope I explain myself well enough.

tldr: I want to pair trait objects with associated consts, but that's not possible. Are there any alternatives to associated consts to link Rust types to enum values in a way that's enforced by the type system?

I'm building a library that wraps a C FFI interface, that controls a physical video device. Some of the structs I need to handle are video streams, which produces video frames of a certain video mode. In the C interface, you consult an int value to see what mode you're in, and infer what the byte layout of the pixels are, etc.

Previous attempts

My initial API featured this struct, used to specify what type of video stream you want, that only loosely improved upon the C library's video mode struct by changing mode from a c_int to an enum (whose value is the c_int that the C library expects).

struct VideoMode {
    format: PixelFormat,
    width: c_int,
    height: c_int,
}

#[repr(i32)] // it won't let me repr(c_int) but whatever
enum PixelFormat {
    RGB888 = 0,
    // etc.
}

Now I want to type-parameterize the eventual video frames with their pixel formats (type ColorPixel888 = [u8; 3];, that sort of thing), but that's different from the enum variants passed into the stream creation function; and I don't know how to link the resulting frame type parameter to a specific enum variant.

What the initial prototype ends up using is a type-level honor system: I, as the programmer, call something like stream.get_frame::<ColorPixel888>(), and hope you get the right one. But there's nothing stopping me from picking the wrong pixel type as the type parameter. A runtime check of assert_eq!(mem::size_of:<P>(), frame.video_mode.bytes_per_pixel) is my only guard rail.

Current version

The disconnect in the previous attempt had to do with the pixel format enum (used to select the video format in the C library) had nothing to do with the concrete type of the pixels that the stream produces. I eventually settled upon a scheme where I moved pixel format selection into the type level, so that I could use a trait and associated types to link "desired video mode" with "output pixel type".

struct VideoMode<P: Pixel> {
    width: c_int,
    height: c_int,
    _video_mode: PhantomData<P>,
}

trait Pixel: Debug {
    // Format is the actual pixel type a frame of this video mode
    // will have: u16, [u8; 3], etc.
    type Format;
    const BYTES_PER_PIXEL: usize;
    // And this const is the value that the create_stream C function takes
    const FFI_ENUM_VALUE: c_int;
}

// Followed by a macro that defines all the mode types with their
// correct pixel formats, numeric values, and byte sizes i.e.
pixel!(ModeTypeName, u16, THE_ENUM_CONSTANT_EXPORTED_BY_THE_C_LIB, 2);

So basically, by creating a stream with a VideoMode that's type-parameterized with some type P: Pixel, then that stream will yield frames parameterized with pixel type P::Format, and the programer will not have to worry about keeping their VideoMode structs and output types in sync. I really like this.

(And it's useful to note that Pixel::Format is not the same thing as what the FFI_ENUM_VALUE represents. GrayScale16 and Depth1MM both return pixels of type u16 but they are completely different data.)

The problem, dyn Pixel, i.e. the reason for this post

So if you go device.create_stream_with_video_mode::<ColorRGB888>(video_mode), you'll get a Stream<ColorRGB888>, and if you eventually call stream.read_frame(), you'll get a Frame<[u8; 3]>. Perfect.

The problem is that a device might not be able to support the specific dimension/framerate/format combination that you request. What you're supposed to do is ask the API for a list of video modes that it supports, then pick one to use as the argument to create_stream. This is where I'm having trouble. I guess the type signature would look like:

impl Stream {
    pub fn supported_video_modes(&self) -> Vec<Box<VideoMode<dyn Pixel<Format=_>>>>;
}

But that won't work because Pixel with an associated const isn't object safe. Nor will it ever be object safe, because the recommended workaround of a helper method needs a &self param, and the Pixel trait will never be instantiated, just used as a param. Not to mention I don't know what to put in that underscore position. And gosh, I don't know what to do now.

The current API works, but requires the programmer to piece together a VideoMode that's valid for their specific device they have plugged in. I sure would like to be able to give them the option of asking the library which modes are valid, and just picking one to use.

Here's where I ask the question

So either

  1. How do I work around the fact that a struct of trait T can't be a dyn trait if it has associated types/consts
  2. Any ideas on an API design that doesn't use associated types/consts, but still somehow links "the parameter you pass in to select a video mode" and "the type of pixel this stream returns" in a type safe way?

Generally in this kind of situation, I would put the different variants in an enum instead of a trait. The reason for this is

  1. The possible variants are known at compile time, so users implementing the trait themselves doesn't make sense.
  2. If you use a trait, the user will not be able to match on the various cases.
  3. There are no issues with object safety.

Note that regarding your #[repr(i32)] pixel format enum, you should watch out with FFI and enums because it's undefined behavior if FFI code returns an int with an invalid value if you interpret it as an enum. Instead you should convert it manually.

2 Likes

If you really want to use traits, you can change the strean api a bit. Instead of listing out all of the types that it supports, you can give a function which returns true if it supports a specific format.

impl Stream {
    pub fn is_supported_video_modes<P: Pixel>(&self, &VideoMode<P>) -> bool;
}