How to statically verify that dimensions match?


#1

I have two structs, Image and Encoder. When giving an Image to Encoder, I need to statically verify that the encoder is configured to accept the width, height, bit depth, and specific format (RGB/BGRA/I420/…) of the image. The latter is easy with an extra type parameter, but how would I statically check that the image has the right width and height for the encoder? Is this even possible right now?


#2

You probably want const generics, which is still being implemented


#3

How would those help if the width and height are user-specified?


#4

If width and height are specified at run time, you’re not going to be able to validate that they’re in bounds at compile time…


#5

But I might be able to verify that they originated from the same source, which would mean that they’re equal. Or maybe, a method on the encoder could be used to create the image, somehow letting the compiler know that the equality check can be skipped for that image.


#6

Keep in mind that Rust compiles a separate copy of each generic function for each combination of type arguments. You wouldn’t want to have a million copies of the encoder for each possible image size.


#7

Yeah, good point. This is my current sketch of how an x264 wrapper might handle colorspaces:

trait Colorspace {
    fn format() -> c_int; // Includes highdepth and vflip as bitflags.
    fn planes() -> c_int; // Number of planes.
}

struct Image<'a, C: Colorspace> {
    width: i32,
    height: i32,
    strides: [c_int; 4],
    data: [*const c_char; 4],
}

struct Rgb; impl Colorspace for Rgb { ... }

fn rgb(width: i32, height: i32, img: &[u8]) -> Image<Rgb> {
    assert_eq!(img.len(), width * 3);
    Image {
        width,
        height,
        strides: [width, width, width, 0],
        data: [...] // Split the image into the three planes.
    }
}

struct Encoder<C: Colorspace> { ... }

impl<C: Colorspace> Encoder<C> {
    fn encode<'a>(&mut self, img: Image<'a>) -> ??? {
        assert_eq!(img.width, self.width);
        assert_eq!(img.height, self.height);
        self.encode_unchecked(img)
    }

    ...
}

fn example() {
    let encoder = ...;
    let image_bytes = ...
    let image = x264::rgb(width, height, image_bytes);
    let result = encoder.encode(image);
}

The main problems I see with it are:

  • It looks convoluted.
  • Even more functions (or exposing an unsafe constructor) are needed if I actually want to use planes that aren’t adjacent in memory.
  • Type level booleans or many more types would be needed for any options.
  • Three asserts per loop.

Is there a better solution to make sure that width, height and format are all the same?


#8

Is this specific struct layout required by C? It’s a very C-like type-unsafe design.


#9

No, I’m trying to make a safe wrapper around the unsafe C type:

typedef struct x264_image_t
{
    int i_csp, i_plane, i_stride[4];
    uint8_t *plane[4];
} x264_image_t;

It looks like I didn’t do a very good job. :slightly_frowning_face:


#10

So C-like way is to have generic data u8 + some other field that defines what type it actually is. Rusty way is to have an enum with fields, e.g.:

enum ImageData {
   RGB(Vec<RGB>),
   YUVPlanes {y: Vec<u8>, u: Vec<u8>, v: Vec<u8>},
}

alternatively you could have completely separate structs for planar and interleaved types, and implement a common trait for both.

struct Interleaved<T> {  // see imgref and rgb crates
   width, height, stride,
   pixels: Vec<T>,
}
// and
struct Planar3<T> {
   planes: [Interleaved<T>; 3],  // allowing subsampling, so each plane has own size and stride
}

#11

If you don’t know whether data will be created on the Rust side (Vec) or C side (*mut), then you can use Cow to support both mixed at runtime, or use a generic container instead of Vec (maybe AsRef<[u8]> would work).


#12

But coming back to your original problem of tying image to encoder instance, it’s probably not worth the complication. I think it’s a “typestate” kind of construct that Rust intentionally doesn’t support, so it’d require some very clever hacking. And it doesn’t seem to worth it, because ultimately you’re saving just a couple of asserts on cold paths of the code.


#13

So here’s a terrible idea

You can use HRTB function bounds to introduce “unique” types into a function body that unify with themselves, but not with each other. By using these types to tag various data structures, you can keep track of the “source” of the data, in order to control which objects are compatible with each other.

All you have to do (*cough* *cough* *hack*) is write your API inside out, so that instead of having a function that creates an Encoder, you have e.g. a function that takes a continuation F: for<'tag> FnOnce(Encoder<'tag>). And deal with crazy lifetime errors when you use the wrong tags together. Perfect, right?

(In the end, I think I probably just basically reinvented @Gankro’s wild technique for bounds-check-elision, only I never actually could figure out how it worked until just now.)

use ::std::marker::PhantomData;

// a type that encodes a lifetime
type Lifetime<'a> = PhantomData<&'a ()>;

// a type that is invariant over 'a.
type Invariant<'a> = PhantomData<&'a mut Lifetime<'a>>;

// 'F: for<T> FnOnce<T>' would be nicer but we're forced to resort
// to lifetimes since those are the only things we can quantify over.
fn with_unique_tag<B, F>(f: F) -> B
where F: for<'tag> FnOnce(Invariant<'tag>) -> B,
{
    // the lifetime used here is arbitrary; the point is that
    // the compiler won't be able to unify it across different
    // calls to this function, no matter what it is (even 'static)
    let invariant: Invariant<'static> = Default::default();
    f(invariant)
}

// Attempts to unify the lifetimes assigned to two Invariants.
fn static_assert_same_tag<'a>(_: Invariant<'a>, _: Invariant<'a>)
{ }

fn main() {
    with_unique_tag(|tag1| {
        with_unique_tag(|tag2| {
            // the tags unify with themselves...
            static_assert_same_tag(tag1, tag1);
            static_assert_same_tag(tag2, tag2);

            // ...but not with each other.
            // ERROR: Cannot infer an appropriate lifetime
            // (yes, that's the error you're stuck with.)
            static_assert_same_tag(tag1, tag2);
        })
    })
}

#14

While we wait for type-level integers, you can use typenum as a stop-gap measure – however that still requires your dimensions to be known at compile time.

An easier way to solve this is making one type depend on the other and reuse its dimensions in the constructor.