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
- How do I work around the fact that a struct of trait
T
can't be adyn
trait if it has associated types/consts - 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?