Traits and trait objects

As I spend more time working with Rust, I find myself hitting more edge cases, and ultimately into learning more about how Rust is implemented.

This weekend I was working on path-tracer, refactoring it to make shapes generic instead of always spheres, and adding explicit light sampling. While doing so I hit some unusual error messages, of the form:

the trait `renderable::Renderable` is not implemented 
    for the type `renderable::Renderable`

A somewhat confusing error message, I’m sure you’ll agree. To understand what it means, one needs to understand the difference between traits, and trait objects.

A trait is the specification of an interface. That interface can contain functions (both member, and non-member), types and constants. A simple example:

pub trait Renderable {
    fn intersect(&self, ray: &Ray) -> Option<f64>;
}

Here we say something can be Renderable if it supports a member function called intersect taking a Ray and returning an optional double-precision floating point number.

Later we may implement this trait for a concrete object, for example a sphere:

struct Sphere { pos: Vec3d, radius: f64 }
impl Renderable for Sphere {
    fn intersect(&self, ray: &Ray) -> Option<f64> {
        ...
    }
}

There are two ways the trait can be used. The first is to accept it as a generic parameter:

fn render<R: Renderable>(obj: &R) {
    ...
}

Here the render function works rather like a templated C++-function, and will be instantiated for each R type used. That is, it’s roughly equivalent to:

// R must implement functions in "Renderable"
template<typename R> void render(const R &obj) {
    ...
}

(ith concepts lite, C++ will be able to restrict the R to being renderable soon.)

Using traits generically is great – the performance is excellent – but of course there comes a time when you need to deal with a heterogenous collection of Renderables at runtime. For example, if one has a scene made up of different types of object, each render call needs to be dispatched based on its runtime type. Enter the second way of using traits – via trait objects:

fn render(obj: &Renderable) {
    ...
}

Looks pretty much the same as the generic usage, right? Behind the scenes though, a lot has changed. For a start, the compiler needs to have some kind of vtable around in order to dispatch calls to Renderable::intersect to the right implementation based on the concrete type of obj. So far this is similar to C++’s virtual method tables. However, where C++ embeds a pointer to the (one and only) vtable inside the object itself, Rust keeps it separately. This is to both to allow traits to be added to existing object and also to allow multiple independant implementations of a trait on an object.

So, when it comes to calling a function that needs one of these vtables, under the hood Rust makes a trait object comprised of two pointers: one to the obj, and the other to the vtable. That trait object is what is passed to the function.

So far so good: we get to choose between compile-time and runtime polymorphism with very similar syntax. Much nicer than C++ templates. But…there’s a catch! In order for Rust to make a trait object, the trait must be “Object Safe”. In its simplest form, it means that the interface itself must have no generic arguments to any functions. Thus:

pub trait Foo {
    fn bar<A>(o: &a);
}

…is a valid trait, but cannot be made into a trait object and thus cannot be used for runtime polymorphism.

But why?

The vtable for Foo must somehow capture a function for every type of A you could supply. As the list of objects types that could be passed to bar() is not known, the compiler would have to do something like instantiate bar() for every object type and then make a vtable entry for each type! This is probably not even possible (I don’t know how much stuff is baked into Rust crates during compilation).

To put it into C++ terms, this would be like trying to write:

struct Foo {
    virtual ~Foo() {}
    template<typename A> virtual void bar(const A&) = 0;
};

…which is ill-formed for basically the same reason!

So…back to my original issue about the trait Renderable not being implemented by the type Renderable. Well, as you may have guessed by now, this is the error you might see if you’ve tried to use an object passed in as a trait as a trait object when the trait itself is not object safe. In my case I wanted to be able to find a random point on a renderable object, using a user-supplied random number generator:

pub trait Renderable {
    fn random_pos<R: Rng>(&self, r: &mut R) -> Vec3d;
}

So far so good. My render method was generic on the Renderable, but somewhere in the body of the code I called another method that took a &Renderable. The compiler tries to construct a trait object to give to that method, and fails with the error described above. You can see the error in the Rust Playground.

So, if you see this somewhat paradoxical error, now you know what it means!

Filed under: Coding Rust
Posted at 14:30:00 BST on 8th June 2015.

What abuse of compute resources looks like

With the partial image and seeding support in my Rust path tracer, I was finally able to do what I had hoped…use some idle time on my work computers to render a “proper” number of samples.

A pretty render
What 127,000 samples per pixel looks like.

Well, there you go then!

Took just over an hour broken up into 1,000 jobs running at idle priority on work’s compute cluster.

Once I’ve finished stylistic tweaks to the Rust code, I’ll put in explicit light tracing, which will speed it up considerably. Then I can start doing samples in the spectral space as well as the time and focus space, and get some really cool results, such as Michael Fogleman‘s awesome Go path tracer.

Filed under: Coding Rust
Posted at 01:50:00 BST on 3rd June 2015.

Error handling in Rust

Previously, I appealed for help with my little Rust path tracer. Thankfully I had some help from Phil Dawes and now at least I “get” error handling, or at least I think I do!

Rust made the brave decision to not support exceptions. Instead, a rich set of features are used to build more explicit error handling:

The only thing remotely exception-like is the last-resort “panic” that prints a diagnostic message and exits the current thread.

Functions that can fail should return a Result<T, E> which itself is an enum holding either a return value of type T (wrapped in Ok), or an error of type E (which is wrapped in Err). Callers can then either match on the success (or otherwise), call unwrap() on the Result to turn it into its T (or panic if it was an error), or call one of the other convenience methods on Result (like is_ok()).

The standard input/output routines all return Result<T, io::Error> so to open a file one might do one of the following:

// this will panic if the file can't be opened
let file = File::open(&name).unwrap();

// this lets us check if the file opened ok
let file = match File::open(&name) {
    // Error? Return from this function with the
    // error.
    Err(x) => return x,
    // All ok? Get the file 
    Ok(f) => f
};

The pattern of returning the error to the caller if it’s an error, or else unwrapping the return value and using it, is so common it is enshrined in a Rust macro, try!. The macro machinery expands try!(X) into basically the same code as the match case above.

One caveat: using try! only works if you’re in a function whose return value is a Result. This first caught me out when copy/pasting examples from Stack Overflow into my fn main() function…the compiler errors were rather confusing.

In my little path tracer’s case I was interested in returning an error from the image merger’s loader. In my first attempt I had rather punted on error handling, choosing instead to panic! on parse errors. I didn’t like this, and now I return an error of my own.

I was returning an io::Result<PartialImage> to propagate the io::Results that the try! on the file routines would return on error. When it came to returning my own errors I was stuck: I couldn’t work out how make my own io::Results.

The solution is to use my own error type, and then make it convertible from io::Result:

enum ImageError {
    IoError(io::Error),
    BadFileError(String)
}

impl From<io::Error> for ImageError {
    fn from(e: io::Error) -> ImageError {
        ImageError::IoError(e)
    }
}

Once you know the trick of wrapping other librarys’ error types in your own, it mostly makes sense.

In a future post I hope to work out a nicer way of parsing the RGB triples using ranges.

Filed under: Coding Rust
Posted at 18:30:00 BST on 2nd June 2015.

Further notes on Rust: a second look

Last time I looked at Rust’s performance vs C++ for a simple path tracer. Since then I’ve been further hacking on the source to get a little extra functionality, like partial rendering and merging (so I can try out some distributed rendering).

I guess the honeymoon is over. I tried a bit of “simple” file manipulation, and I’m finding all sorts of trickinesses. In this post I am referring to this version of path-tracer on GitHub. I’m sure by the time anyone reads this I’ll have found workarounds, so check the most recent version to see how I’m getting on.

My path tracer can now spit out intermediate partial renders, each containing a set of samples, which are then merged together at the end. Although the image library I’m using for PNG support can probably do something similar, as an experiment I output my own simple text-based format of the form:

width height #samples
R G B R G B ... f64 triples each sample line 1
R G B R G B ... f64 triples each sample line 2
R G B R G B ... etc

Generating the file is pretty simple, but loading it back again to merge it has been a major source of frustation.

My best effort is here. I welcome suggestions to improve it, notably:

More generally I found the cargo setup to make a package with two binaries very awkward, painting myself into a corner multiple times before realising if I just name things “correctly” and get rid of all my crap in Cargo.toml, it kinda “just works”. Though now I need to run each binary by name in cargo run --bin path_tracer --release -- my args here.

Anyway, I’m still liking Rust, but I think I need to get my head around how to use it more effectively. I’ve asked for help from Phil Dawes (author of racer – a Rust code completion tool), who works for the same company as me, so hopefully I’ll be able to post a follow-up with improvements soon!

Filed under: Coding Rust
Posted at 14:00:00 BST on 1st June 2015.

About Matt Godbolt

Matt Godbolt is a developer working on cool secret stuff for DRW, a fabulous trading company.