May 20, 2020 in Programming7 minutes
Lately I’ve been working on graphics programming in Rust, as a continuation of my first steps with the language. As part of this work, I created a type I created called Vec3f
, to hold cartesian coordinates for a given vector:
In the natural course of this work, I needed to add certain methods for this type to allow me to perform calculations like cross product, and dot/scalar product. These functions are fairly straightforward and read information from the instance of Vec3f
(self
), perform some kind of calculation, and return some kind of result, usually a new Vec3f
instance, or a simple f32
.
In some cases, I want to do more. For instance, a common task in graphics programming is to “normalize” a vector - that is to actively change its properties so that its direction is unchanged, but its magnitude is reduced to 1. Such a vector is also referred to as a unit vector.
This is done by multiplying each property by the result of dividing 1.0
over the original magnitude of the vector. In my original attempt, I came up with this:
To demonstrate, we can call this function simply, by first creating an instance of Vec3f
called v
, with some made-up coordinates, and then invoking its normalize()
method, which should change the coordinates in-place to ensure the vector is normalized.
However, the output shown by the println
statement seems to indicate something’s not quite right:
For some reason, the coordinates for our vector have not changed. To begin to troubleshoot this, I added a debug statement to the end of the normalize()
function, and it seems that the coordinate properties for self
have indeed been changed at that location. However, our debug statement in the main()
function doesn’t show these changes - it still shows the original values, unaffected.
What gives?!?
It turns out this is yet another instance where Rust’s ownership model is trying to keep me from doing something stupid.
The first thing that tipped me off to a problem is this warning from the compiler:
It was strange to me that Rust was telling me I didn’t need to declare this as mutable. The normalize()
function absolutely should be mutating v
- that is its whole purpose. So this keyword should be necessary.
You may notice that I’m annotating my Vec3f
type to automatically derive some implementations, namely:
If we remove this and try to compile, you’ll immediately see why:
Prior to my attempt to implement the normalize()
function, I added this annotation so that we could seamlessly use the properties of Vec3f
for calculations. Thus far, we just needed to return new values, such as f32
type, based on calculations we can obtain simply by reading from the properties of the vector. We didn’t need to change them, just read them.
These are object methods, which use the first parameter self
(very similar to the way Python does things). One of the rules of Rust Ownership is that a value can only have one owner. Since the Vec3f
type originally had no method for copying or cloning itself (which is the case for any type without an annotation), it moved the ownership for the value into the method.
Because of this behavior, any code after this move is unable to continue to use the value. We’re not even able to use the println
macro to print the result after the normalize()
function:
So, clearly we need the copy functionality, at least the way things are currently implemented. Doing so provides the compiler with an alternative to moving the ownership for these values into a scope where the rest of our code is left hanging out to dry.
Okay so we’ve figured out that to get our code to compile, we need to annotate our structs so that we automatically get Copy/Clone capabilities for them. However, this doesn’t solve our original problem, which is that our object method didn’t appear to be actually mutating our Vec3f
instance like we wanted.
Well, since we now know that within the context of this method, self
is actually a copy of this value, it suddenly becomes obvious that all we’re doing is mutating this copied value, not the original instance, which is still owned by the variable v
in our main()
function.
There is another way, that doesn’t require a move, or a copy, and that is to declare self
in this function as a mutable reference. We can do this by adding an ampersand before the mut
keyword:
Prior to this change, since self
was a copy of the value, all we were doing was declaring that we wanted to be able to mutate that copy. By adding the ampersand, we’re allowing the normalize
function to actually borrow ownership of the original value. Together with the mut
keyword, we’re now able to make the changes successfully. Re-running this example shows a normalized vector:
Note that rust only allows one mutable reference to a variable at a time, but fortunately that’s all we need. Our normalization function borrows the reference, makes a quick change, and gives it back. The calling code blocks until this is done, at which point the scope the mutable reference was created in is gone.
I’m still new to Rust, and I have to say that I have read the chapter on ownership and borrowing a few times now, and I don’t think I really “got it” until this problem bit me. Nothing like a few battle scars to really hammer home the hard lessons! :)
Hopefully it helped you.