Darrien's technical blog

Documenting the technical stuff I do in my spare time

So you want to write object oriented Rust

Whether you wanted to find out about object oriented Rust yourself, or you wanted to see why in the world I’m talking about object oriented rust, you are here. And so let us talk about object oriented Rust.

Object oriented Rust is not so outlandish. Many folks think of Rust as a functional language, and while there are plenty of functional paradigms in Rust, many of those paradigms are also available to other languages in one way or another.

Most folks would not call Java a functional language, and yet many of the features cited that make Rust a functional language are available as libraries for Java. If you want algebraic data types, there’s a library for that. If you want pattern matching, there’s a library for that too1.

The absence or presence of such features does not make a language object oriented or functional, there are plenty of ways to stamp features from one langauge onto others.

With that said, given Rust is intended to be a functional replacement to C++, plenty of object oriented features exist in Rust. You don’t have to leave the standard library to access them either!

Because of many of the restrictions and lack of a GC in Rust, there are a number of nuances with how Rust handles OOP. It can certainly work like Java if you really want it to, but sometimes it can take a little finagling.

The remainder of this post will talk about OOP in the context of Rust itself, and common pitfalls you may hit along the way while writing object oriented Rust or using object oriented patterns.


Anyway enough talk, let’s write some Rust. Rust has a concept of traits, which are the Java equivalent of interfaces2. They define a common set of methods to be used across object types. Let’s throw together some traits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
trait Worker {
    fn receive_pay(&self) -> u32;
}

trait SoftwareEngineer: Worker {
    fn write_code(&self) -> String;
}

trait Astronaut: Worker {
    fn see_the_moon(&self);
}

These two traits are friends. SoftwareEngineer is a SuperTrait that encapsulates the functionality of Worker. Implementors must also implement worker if they would like to implement SoftwareEngineer. So far this looks like piecemeal composition. Nothing exciting yet.

Let’s get some implementors.

First we are making some structs to hold traits for the implementors.

1
2
3
4
5
6
7
struct RustDev {
    balance: i32,
}

struct NasaWorker {
    balance: i32,
}

And then we implement the above traits on them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
impl Worker for RustDev {
    fn receive_pay(&mut self) -> i32 {
        self.balance += 50000;
        self.balance
    }
}

impl SoftwareEngineer for RustDev {
    fn write_code(&self) -> String {
        r#"panic!("At the software-co")"#.to_owned()
    }
}

impl Worker for NasaWorker {
    fn receive_pay(&mut self) -> i32 {
        self.balance += 1000000;
        self.balance
    }
}

impl Astronaut for NasaWorker {
    fn see_the_moon(&self) {
        println!("wow that's cool");
    }
}

Still nothing exciting here. So let’s change that a little. Now I want to start using these types and we can really start leveraging our traits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn main() {
    let engineer = RustDev { balance: 0 };
    let astronaut = NasaWorker { balance: 0 };
    println!("Engineer's balance: {}", pay_worker(engineer));
    println!("Astronaut's balance: {}", pay_worker(astronaut));
}

fn get_astronaut() -> impl Astronaut {
    NasaWorker { balance: 0 }
}

fn get_engineer() -> impl SoftwareEngineer {
    RustDev { balance: 0 }
}

fn pay_worker(mut worker: impl Worker) -> i32 {
    worker.receive_pay()
}
1
2
3
4
$ rustc trait-test.rs
$ ./trait-test
Engineer's balance: 50000
Astronaut's balance: 1000000

You can see we are generic over Worker and receive pay on both of them. Despite not needing it, you’ll note I made the pay_worker method take ownership of the Worker argument. It doesn’t even use a reference.

This is the simplest and fastest way to pass generic objects around. It doesn’t use dynamic dispatch. Rust knows ahead of time what your type is and just calls the method.

Anyway let’s keep going with the examples. I want to get a bunch of these workers, put them in a collection, and give them all pay.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let astronaut = get_astronaut();
    let engineer = get_engineer();
    give_em_pay(vec![astronaut, engineer]);
}

fn give_em_pay(mut workers: Vec<impl Worker>) {
    workers.iter_mut().for_each(|worker| {
        worker.receive_pay();
    });
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ rustc trait-test.rs
error[E0308]: mismatched types
  --> trait-test.rs:50:33
   |
50 |     give_em_pay(vec![astronaut, engineer]);
   |                                 ^^^^^^^^ expected opaque type, found a different opaque type
...
63 | fn get_engineer() -> impl Worker {
   |                      ----------- the found opaque type
   |
   = note:     expected type `impl Worker` (opaque type at <trait-test.rs:59:23>)
           found opaque type `impl Worker` (opaque type at <trait-test.rs:63:22>)
   = note: distinct uses of `impl Trait` result in different opaque types

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

Well that’s interesting. Everything implements Worker, but it’s just not allowed. Even more interesting, if you replace the two SoftwareEngineer + Astronaut with just Astronaut, it works!

1
2
3
4
5
6
fn main() {
    let astronaut1 = get_astronaut();
    let astronaut2 = get_astronaut();
    give_em_pay(vec![astronaut1, astronaut2]);
    println!("The workers are paid!");
}
1
2
3
4
5
6
$ rustc trait-test.rs
warning: function is never used: `get_engineer`
...

$ ./trait-test
The workers are paid!

That’s because static dispatch doesn’t allow for collections of implementors unless the implementors are all the same. All impl Traits must eventually compile down to the same type, meaning there is no way to carry a collection of traits using static dispatch if they compile to different implementations.

This is one of the most frustrating behaviors of static dispatch, as it makes it very difficult to pass ownership of objects around in a generic way. If you need ownership of said objects in a collection, you’ll need to use dynamic dispatch.

Dynamic dispatch you say?

Yes, dynamic dispatch. Smarter folks have described it than me, so here is one of their definitions.

Copied shamelessly from Wikipedia:

In computer science, dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at run time. It is commonly employed in, and considered a prime characteristic of, object-oriented programming (OOP) languages and systems.

In short, the way dynamic dispatch works is:

  1. Have a pointer to an object
  2. Carry a vtable with a set of pointers to that object’s implementation of methods.
  3. Figure out which to call at runtime through this indirection

This indirection has a cost. It’s all figured out at runtime which makes it a little slower. However we get vastly increased flexibility with dynamic dispatch.

So how does it work with Rust?

The TL/DR is we have to throw our data behind a pointer. Heap or stack allocated, method calls must be behind a pointer to the trait, and rustc will figure out what to do from there.

Let’s change up our example a little:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn main() {
    let mut astronaut = get_astronaut();
    let mut engineer = get_engineer();

    give_em_pay(vec![&mut astronaut, &mut engineer]);
    println!("The workers are paid!");
}

// notice the dyn  ------------------∨
fn give_em_pay(mut workers: Vec<&mut dyn Worker>) {
    workers.iter_mut().for_each(|worker| {
        worker.receive_pay();
    });
}
1
2
3
$ rustc trait-test.rs
$ ./trait-test
The workers are paid!

Run and no more compile errors! The only “real” change to the code was from impl -> dyn This has performance implications, but also opens up a world of possibilities. For instance, you can see we now have an engineer and an astronaut in the same array now.

We’re still using references here, but if we wrapped our astronaut and engineer in a Box, we would own them in the vec. Nice!

Holding traits

No not like a hug, but in a struct. Let’s try it out real quick. Does this work?

1
2
3
struct WorkerHolder {
    worker: impl Worker,
}
1
2
3
4
5
6
7
8
$ rustc trait-test.rs
error[E0562]: `impl Trait` not allowed outside of function and inherent method return types
  --> trait-test.rs:48:13
   |
48 |     worker: impl Worker,
   |             ^^^^^^^^^^^

error: aborting due to previous error

Well no. But that’s not because it doesn’t work. The error is a little disingenuous.

impl is syntactic sugar for a more verbose syntax that specifies data is generic over a type. For some reason the impl syntactic sugar does not work here. The proper syntax to make this work looks like so:

1
2
3
struct WorkerHolder<T: Worker> {
    worker: T,
}

There isn’t much more excitement to talk about for static dispatch here, so let’s move back to dynamic to discuss a few other quirks with it.

This is the proper syntax for holding a traits using dynamic dispatch:

1
2
3
4
5
6
7
struct ReferenceWorkerHolder<'a> {
    worker: &'a dyn Worker,
}

struct BoxWorkerHolder {
    worker: Box<dyn Worker>,
}

Owning and cloning

Let’s take a closer look at the boxed implementation. I’d like to pass the worker into a method that looks like this:

1
2
3
fn do_crazy_stuff_with_worker(worker: Box<dyn Worker>) {
    // fly to mars or something, I dunno
}

Let’s make an astronaut and do crazy stuff with him twice. This method wants an instance of the astronaut, and so we comply.

1
2
3
4
5
6
7
8
fn main() {
    let mut holder = BoxWorkerHolder {
        worker: Box::new(get_astronaut()),
    };

    do_crazy_stuff_with_worker(holder.worker);
    do_crazy_stuff_with_worker(holder.worker);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ vim trait-test.rs
$ rustc trait-test.rs
error[E0382]: use of moved value: `holder.worker`
  --> trait-test.rs:61:16
   |
60 |     pay_worker(holder.worker);
   |                ------------- value moved here
61 |     pay_worker(holder.worker);
   |                ^^^^^^^^^^^^^ value used here after move
   |
   = note: move occurs because `holder.worker` has type `std::boxed::Box<dyn Worker>`, which does not implement the `Copy` trait

Oh no, lifetime errors. That’s no good. Luckily the errors look simple. We’re giving the method ownership of our object and then call it again. Of course we don’t own it anymore.

Since we just want to give it an instance of our worker we can clone it, right?

1
2
3
4
5
6
7
8
fn main() {
    let mut holder = BoxWorkerHolder {
        worker: Box::new(get_astronaut()),
    };

    do_crazy_stuff_with_worker(holder.worker.clone());
    do_crazy_stuff_with_worker(holder.worker.clone());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ rustc trait-test.rs
error[E0599]: no method named `clone` found for struct `std::boxed::Box<(dyn Worker + 'static)>` in the current scope
   --> trait-test.rs:60:30
    |
1   | trait Worker {
    | ------------
    | |
    | doesn't satisfy `dyn Worker: std::clone::Clone`
    | doesn't satisfy `dyn Worker: std::marker::Sized`
...
60  |     pay_worker(holder.worker.clone());
    |                              ^^^^^ method not found in `std::boxed::Box<(dyn Worker + 'static)>`
    |
   ::: /home/darrien/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:160:1
    |
160 | pub struct Box<T: ?Sized>(Unique<T>);
    | ------------------------------------- doesn't satisfy `std::boxed::Box<dyn Worker>: std::clone::Clone`
    |
   ::: /home/darrien/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/clone.rs:122:8
    |
122 |     fn clone(&self) -> Self;
    |        -----
    |        |
    |        the method is available for `std::sync::Arc<std::boxed::Box<(dyn Worker + 'static)>>` here
    |        the method is available for `std::rc::Rc<std::boxed::Box<(dyn Worker + 'static)>>` here
    |
    = note: the method `clone` exists but the following trait bounds were not satisfied:
            `dyn Worker: std::marker::Sized`
            which is required by `std::boxed::Box<dyn Worker>: std::clone::Clone`
            `dyn Worker: std::clone::Clone`
            which is required by `std::boxed::Box<dyn Worker>: std::clone::Clone`

Oh no, rustc is not happy with us. Zippy lime has a good article on Sized and Clone and all that jazz. If you’d like to read a longer more detailed explanation about cloning boxes, it’s a good read. With that said, let’s delve in a little bit here too.

We want to clone the data in our box, and put it in another box. Unfortunately that doesn’t work out of the box (eh? get it? ;) please don’t leave)

Box will implement clone but only if the object inside implements clone. Straight from the docs:

1
2
impl<T> Clone for Box<T> where
    T: Clone,

The problem with this and traits, is that to implement clone, the type you’re cloning must implement sized. clone doesn’t know we want to clone what’s inside the box, and put it right away in another box. It thinks we’re going to take unsized data and try to put it on the stack which rust cannot and will not handle.

In order to clone a box (or rather, what we’d really like is: to clone what is inside the box, allocate new memory in the heap for it, and return that new box), we must venture outside the standard library to a crate called dyn-clone3. which does exactly as we’d like.

So long as all implementors of the trait also implement clone, we may implement dyn-clone on our trait and clone the data inside the box to another box.

The actual changes might look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// add DynClone
use dyn_clone::DynClone;

// add clone to our implementors
#[derive(Clone)]
struct RustDev {
    balance: i32,
}

#[derive(Clone)]
struct NasaWorker {
    balance: i32,
}

// Have worker implement DynClone
trait Worker: DynClone {
    fn receive_pay(&mut self) -> i32;
}

And finally we can clone the box data:

1
2
3
4
5
6
7
8
fn main() {
    let mut holder = BoxWorkerHolder {
        worker: Box::new(get_astronaut()),
    };

    pay_worker(dyn_clone::clone_box(&*holder.worker);
    pay_worker(dyn_clone::clone_box(&*holder.worker);
}

Seems like a bit of a hassle, but if you want to easily clone your instances inside boxes, this is more or less the only way.

Depending on the use, we may have been able to get away with using Rc or Arc, but sometimes you need ownership.

Upcasting

Well now we can hold our traits. Can we pass them around? Yes and no.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
    let nasa_worker: Box<NasaWorker> = get_nasa_worker();
    let astronaut: Box<dyn Astronaut> = get_astronaut();

    pay_worker(nasa_worker);
    pay_worker(astronaut);
}

fn get_nasa_worker() -> Box<NasaWorker> {
    Box::new(NasaWorker { balance: 0 })
}

fn get_astronaut() -> Box<dyn Astronaut> {
    Box::new(NasaWorker { balance: 0 })
}

fn pay_worker(mut worker: Box<dyn Worker>) {
    worker.receive_pay();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ rustc trait-test.rs
error[E0308]: mismatched types
  --> trait-test.rs:62:16
   |
62 |     pay_worker(astronaut);
   |                ^^^^^^^^^ expected trait `Worker`, found trait `Astronaut`
   |
   = note: expected struct `std::boxed::Box<(dyn Worker + 'static)>`
              found struct `std::boxed::Box<dyn Astronaut>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

Well that’s interesting. The boxed struct is passed into the function fine, but, the boxed trait is not. The implementations are the same, so why doesn’t it work?

It actually should work. It’s legitimate fine code. There’s an issue about supporting upcasting here.

Frustrating, so how do you get around this? Well it turns out it isn’t too hard, although you have to waste a whole function call on the conversion which is a shame.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
trait Astronaut: Worker {
    fn see_the_moon(&self);
    fn as_worker(self: Box<Self>) -> Box<dyn Worker>;
}

impl Astronaut for NasaWorker {
    fn as_worker(self: Box<Self>) -> Box<dyn Worker> {
        self
    }
    // ...rest of impl
}

And now the example works if you do:

1
2
3
4
5
6
7
fn main() {
    let nasa_worker: Box<NasaWorker> = get_nasa_worker();
    let astronaut: Box<dyn Astronaut> = get_astronaut();

    pay_worker(nasa_worker);
    pay_worker(astronaut.as_worker());
}

Definitely a little grody, but you gotta do what you gotta do.

The kind folks on reddit pointed me to a handy crate that hides all this behind a proc macro if you don’t feel like implementing it yourself. You can check it out here.

Methods without self

Occasionally you may want to add a method to a trait that does not reference self. The most common case I’ve seen is a constructor, but there are plenty of other reasons you may want to add one. Let’s try to add one to our Astronaut.

1
2
3
4
trait Astronaut: Worker {
    fn new() -> Self;
    // ...rest of impl
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/tmp rustc trait-test.rs
error[E0038]: the trait `Astronaut` cannot be made into an object
  --> trait-test.rs:78:23
   |
5  | trait Astronaut: Worker {
   |       --------- this trait cannot be made into an object...
6  |     fn new() -> Self;
   |        --- ...because associated function `new` has no `self` parameter
...
78 | fn get_astronaut() -> Box<dyn Astronaut> {
   |                       ^^^^^^^^^^^^^^^^^^ the trait `Astronaut` cannot be made into an object
   |
help: consider turning `new` into a method by giving it a `&self` argument or constraining it so it does not apply to trait objects
   |
6  |     fn new() -> Self where Self: Sized;
   |                      ^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0038`.

Well that’s no good. With no self parameter, rustc tells us we can’t add the method to our trait. Since there is no self parameter in the method, rustc won’t know what implementation to call. This makes the Astronaut object unconstructable.

Luckily this is easily fixable. rustc even gives us the answer in the error:

1
2
3
4
5
6
7
trait Astronaut: Worker {
    fn new() -> Self
    where
        Self: Sized;
    fn see_the_moon(&self);
    fn as_worker(self: Box<Self>) -> Box<dyn Worker>;
}

By adding a where Self: Sized; we tell rust this method must only be available to objects that are sized. In short, it must only be available to implementations. Once that’s done, we can compile away and it all works!

And that’s that!

If you’ve got through all of this, you’ve now got a pretty solid idea of how to do simple object oriented programming in rust.

If you liked this article, feel free to check out another one! I’ve stuck to my unofficial schedule of one post a month, and all the others talk about something that has to do with rust.


  1. These libraries are really good. If you have to write Java for your day job, they erase a lot of boilerplate and really do a great job bringing functional features to Java. We heavily use these at HubSpot. ↩︎

  2. I don’t want to hear any complaining or: “Well technically’s” that source is straight from rust-lang.org ↩︎

  3. Luckily it is a crate made by dtolnay, so it might as well be standard library quality. ↩︎

Share on: