0
votes

I have the following Rust Playground permalink

which is from my following Ray Tracing in a Weekend.

At the point of implementing materials I chose to create a trait Material.

My thinking was that objects in the world would have a material attribute and upon a ray Hit the Hit can look at the object and ask for the material on demand. This is working fine for my Normal trait which follows a similar thinking. I implemented this all with dynamic dispatch although I think I grasp enough to have done it statically with trait bounds as well.

In the linked code, you see line 13 I'm requesting those which implement Normal have a method which will return a Material. This then suggests that now Normal is no longer eligible to be a trait object error[E0038]: the traitNormalcannot be made into an object

If I understand correctly from such questions as this it seems that since generics are monomorphized the Rust runtime could no feasibly leak up the appropriate method for material given there could ostensibly be one a different one for each type implementing Normal? This doesn't quite click with me as it seems that, put in the same position as the Rust runtime, I would be able to look at the Normal implementer I have in hand at a moment, say Sphere and say I will look in Sphere's vtable. Could you explain where I am wrong here?

From there, I tried to simply fight with the compiler and went to static dispatch. lines 17-21

struct Hit<'a> {
    point: Vec3,
    distance: f32,
    object: &'a dyn Normal,
}

became

struct Hit<'a, T: Normal> {
    point: Vec3,
    distance: f32,
    object: &'a T,
}

and from there I am left trying to plug hole after hole with what seems like no end in sight.

What design choices can I do differently in addition to learning what is fundamentally wrong with my current understanding?

2

2 Answers

1
votes

put in the same position as the Rust runtime, I would be able to look at the Normal implementer I have in hand at a moment, say Sphere and say I will look in Sphere's vtable

Except there's nothing the Rust runtime can do here.

In fact, Rust don't have the runtime in the sense of "something executing the code". Rust runtime only performs the setup and cleanup task, but as long as the control flow is somewhere inside your main function, you're on your own (and in no_std environments, even this won't exist). So, every dynamic dispatch must be baked into the type, by placing the vtable pointer next to data pointer - see this documentation for a bit more details.

But, since the generics are, as you've already stated, monomorphized, there won't be one fn material for every implementation of Normal: there will be an unknown, potentially infinite family of these functions, one for each type implementing Material. Note the "unknown, potentially infinite" bit: since you can't leak private parts, if the Normal trait is public, the Material trait must be public too - and then nothing will prevent the user of your code to add another implementation of Material, not known to your code, which simply cannot be baked into the vtable of dyn Normal.

That's why generic methods are not object-safe. They can't fit into the trait object vtable, since we don't know them all when trait object is created.

1
votes

I may be missing something, but I think you could - at least from what I've seen - follow your path further.

I think you could change this function:

fn material<T: Material>(&self) -> T;

As it stands, it says: Any Normal offers a function material where the caller can specify a Material that the function will return.

But (I think) you want to state is: Any Normal has a material that can be requested by the caller. But the caller has no right to dictate any Material that will be returned. To translate this to code, you could state:

fn material(&self) -> &dyn Material;

This tells that material returns a Material (as a trait object).

Then, Sphere could implement Normal:

impl<'a> Normal for Sphere<'a> {
    fn normal(&self, point: &Vec3) -> Ray {
        Ray::new(point, &(point - &self.center))
    }
    fn material(&self) -> &dyn Material {
        self.material
    }
}

Link to playground.