Rust's Ownership model for JavaScript developers

New to Rust? Check out myYouTube channel or myfreeintroduction course on Egghead!

It’s been roughly one year ago since we organized Hannover’s firstRust meetup. Time has passed on andnickel has grown into a really nice web application framework with a very active community and 31 individual contributors as of the time of writing this.

We also createdclog a changelog generator that started as a straight port ofconventional-changelog but has since moved on to follow it’s own ideas and currently powers projects such as nickel andclap.

I like to point out that both projects wouldn’t have been where they are today without the help of lots of helping hands from a bunch of smart people.

While we wrote a lot about Angular and Git in this blog already, we didn’t actually took the time to explore Rust.

Let’s change that and start with baby steps. Many readers of this blog are familiar with JavaScript so let’s explore a core concept of Rust from the perspective of a JavaScript developer.

Memory management

Most languages (JavaScript included) use a garbage collector to ensure memory safety.

Well then, what’s the job of a garbage collector anyway? Basically it frees up memory that isn’t used anymore, that is, memory that nothing in the program points to anymore. Traditionally languages that are not memory safe such as C and C++ delegate that work to the developer. As we all know humans aren’t free from failure and so here are some examples of problems that may arise with manual memory management

  • access to memory that has already been freed
  • trying to free memory that has already been freed (double free)
  • not freeing memory at all that rather should have been freed (memory leak)

The concept of ownership in Rust

Rust doesn’t use a garbage collector while still being 100 % memory safe. So how does that work and how does it affect the way we write our code?

Let’s explore it by comparing some simple JavaScript code (written in ES6) with it’s Rust counterpart.

classProduct{}classConfig{constructor(debugMode){this.debugMode= debugMode;}}classProductService{constructor(config){this._config= config;}getProduct(id){if(this._config.debugMode){      console.log('retrieving product for id'+ id)}returnnewProduct();}}classBasketService{constructor(config){this._config= config;}addProduct(product){if(this._config.debugMode){      console.log('adding product %O', product)}}}let config=newConfig(true);let productService=newProductService(config);let basketService=newBasketService(config);var product= productService.getProduct(1);basketService.addProduct(product);

It’s a simple e-commerce example with four different classes working hand in hand. We have aProduct class without any functionality because it’s sole purpose is to represent a product in this demo context. Then there’s aConfig class which may contain a bunch of configurations such as API endpoints or simply adebugMode flag as in our simple example. And last but not least do we have aProductService to retrieve products from and aBasketService to put products into a shopping basket.

Let’s fokus on what follows after the definition of those classes.

let config=newConfig(true);let productService=newProductService(config);let basketService=newBasketService(config);let product= productService.getProduct(1);basketService.addProduct(product);

We create an instance of aConfig and pass it to both services. The last two lines show how the two services are used. Easy enough, right?

Let’s write the same thing in Rust but instead of directly jumping to the final version let me take you on a journey to illustrate the process of how to get there.

We leave out thegetProduct andaddProduct methods for our first implementation as they are just a distraction at this point.

struct Product;struct Config{    debug_mode: bool}struct ProductService{    config: Config}struct BasketService{    config: Config}impl ProductService{fnnew(config: Config)-> ProductService{        ProductService{            config: config}}}impl BasketService{fnnew(config: Config)-> BasketService{        BasketService{            config: config}}}fnmain(){let config= Config{ debug_mode:true};let product_service= ProductService::new(config);let basket_service= BasketService::new(config);}

The first thing to notice here is that Rust has no classes but instead has structs. It’s out of the scope of this article to discuss the differences though. The second thing to notice is that methods aren’t written in the struct definition but are attached to a struct through animpl block instead.

Also does Rust not know anyconstructor concept. Instances of structs can simply be made by writing out the structs name followed by curly braces and a body that initializes all of the structs fields. However it’s a common pattern to add a “static”new method to the struct that wraps the initialization code. Thisnew method is quite compareable to theconstructor in our ES6 classes.

To get things going we need to put the code that creates a config and both services in themain function.

Ok, so we have our first version to try. That wasn’t all that hard, was it? But no, what’s that? When we try to run the code the compiler tells us that something isn’t quite right.

src/main.rs:37:45: 37:51 error: use of moved value: `config`src/main.rs:37     let basket_service = BasketService::new(config);                                                           ^~~~~~src/main.rs:36:47: 36:53 note: `config` moved here because it has type `Config`, which is non-copyablesrc/main.rs:36     let product_service = ProductService::new(config);

The compiler disallows usage ofconfig in line 37 because it moved in line 36. Uhm..what’s a move? Let’s go back to how this all started. We were talking about memory management and how Rust assures 100 % memory safety without the usage of a garbage collector.

When we look back at the JavaScript version we can see that there are three places in our program that hold a reference to theconfig. Each service holds a reference as well as the calling code that createsconfig in the first place.

Since JavaScript is garbage collected we don’t put much thought into that. A garbage collector will just regulary run checks and if it discovers that there is no reference anymore that points to the memory that was allocated for theconfig, it will free it up.

Rust doesn’t have a garbage collector but it doesn’t force you to manage the memory manually either. Instead it creates new rules to enforce memory safety without garbage collection, namely “Ownership”.

Being the owner of an object means that you (and only you) own the right to destroy it.

Let’s see what that really means in the context of our program. The comments explain what happens line by line.

fnmain(){let config= Config{ debug_mode:true};// at this point config is owned by the `main` function// which also means the memory would be freed// at the end of the main functionlet product_service= ProductService::new(config);// at this point config is owned by the `new` method.// So the main method is no longer the owner of `config`// and further use of `config` is prohibited// config can't be used here because `main` doesn't own// it any morelet basket_service= BasketService::new(config);}

You may be wondering why we can’t just continue to useconfig without being the owner. The point is that since thenew method is now the new owner it may just decide to free up the memory. Keep in mind that the owner has the right to destroy the thing that it owns (either explicitly or implicity when it goes out of scope).

If we were allowed to useconfig in the last line the memory may already be freed and hell breaks loose. The rust compiler prevents us from a potential runtime crash here.

The concept of borrowing

The good news is that we don’thave to transfer ownership each time we pass something to another method. We can just lend out a reference instead.

Before we refactor our code to have the services borrow the config, we will temporarily simplify the code one last time to make it obvious why the move happens.

struct Product;struct Config{    debug_mode: bool}struct ProductService;struct BasketService;impl ProductService{fnnew(config: Config)-> ProductService{        ProductService}}impl BasketService{fnnew(config: Config)-> BasketService{        BasketService}}fnmain(){let config= Config{ debug_mode:true};let product_service= ProductService::new(config);let basket_service= BasketService::new(config);}

We removed the config from both services so that thenew methods still takes theconfig as parameter but doesn’t use it at all. We are still running into the same error.

src/main.rs:27:45: 27:51 error: use of moved value: `config`src/main.rs:27     let basket_service = BasketService::new(config);                                                           ^~~~~~src/main.rs:26:47: 26:53 note: `config` moved here because it has type `Config`, which is non-copyablesrc/main.rs:26     let product_service = ProductService::new(config);

The reason for that lies in the method signature ofnew.

fnnew(config: Config)-> ProductService

This method signature says: “I’m a method that takes ownership of aConfig and returns aProductService“.

But we can change it to borrow a reference instead.

struct Product;struct Config{    debug_mode: bool}struct ProductService;struct BasketService;impl ProductService{fnnew(config:&Config)-> ProductService{        ProductService}}impl BasketService{fnnew(config:&Config)-> BasketService{        BasketService}}fnmain(){let config= Config{ debug_mode:true};let product_service= ProductService::new(&config);let basket_service= BasketService::new(&config);}

Whew, this compiles! The&Config as the parameter type means that it now borrows a reference instead of taking ownership. Themain method continues to be the owner with this change.

But there’s another thing that we changed. Because thenew methods now expect a reference instead of the actual type, we need to change the call site, too.

let product_service= ProductService::new(&config);let basket_service= BasketService::new(&config);

The leading& beforeconfig means that we pass the memory address to the config instead of passing the actual data. And that brings us closer to our JavaScript version which also just passes a reference toconfig under the cover.

Let’s change our code back to store the config in the services so that the service methods can have access to it.

struct Product;struct Config{    debug_mode: bool}struct ProductService{    config:&Config}struct BasketService{    config:&Config}impl ProductService{fnnew(config:&Config)-> ProductService{        ProductService{            config: config}}}impl BasketService{fnnew(config:&Config)-> BasketService{        BasketService{            config: config}}}fnmain(){let config= Config{ debug_mode:true};let product_service= ProductService::new(&config);let basket_service= BasketService::new(&config);}

Unfortunately this gives us another error.

src/main.rs:8:13: 8:20 error: missing lifetime specifier [E0106]src/main.rs:8     config: &Config                          ^~~~~~~src/main.rs:11:13: 11:20 error: missing lifetime specifier [E0106]src/main.rs:11     config: &Config

Rust’s memory management relies on a concept of lifetimes to track references. Most of the time you won’t even notice it because Rust lets us omit most lifetime annotations. But there are cases where Rust needs lifetime annotations such as when defining structs that hold references.

Since we changed our services to store a reference to aConfig instead of theConfig itself rusts expect us to annotate our services with lifetime annotations.

struct Product;struct Config{    debug_mode: bool}struct ProductService<'a>{    config:&'a Config}struct BasketService<'a>{    config:&'a Config}impl<'a> ProductService<'a>{fnnew(config:&Config)-> ProductService{        ProductService{            config: config}}}impl<'a> BasketService<'a>{fnnew(config:&Config)-> BasketService{        BasketService{            config: config}}}fnmain(){let config= Config{ debug_mode:true};let product_service= ProductService::new(&config);let basket_service= BasketService::new(&config);}

Whew, that’s a lot of new syntax that we haven’t seen yet.

Basically what the'a lifetime annotation says is that theProductService can’t live longer than the reference to theConfig that it contains. Rust doesn’t infer that constrain for structs by itself so it needs us to bring clarity. The same helds true for theBasketService as it also keeps a reference to theConfig.

The'a is really only a name that we get to choose, we could have picked'config but short single letter names are mostly used among the Rust community.

We need to use the life time annotation in theimpl blocks as well as those are written for theProductService andBasketService which introduce those lifetimes. Please note that the'a of theProductService is independend of the'a of theBasketService we could have picked different names for both.

A deep dive into the topic of lifetimes is out of scope for this article but we’ll make sure to cover them in more detail with follow up posts.

Now that we got things working with the minimal code needed let’s jump to the final version which introduces theget_product andadd_product methods to the services.

struct Config{    debug_mode: bool}#[derive(Debug)]struct Product;struct ProductService<'a>{    config:&'a Config}struct BasketService<'a>{    config:&'a Config}impl<'a> ProductService<'a>{fnnew(config:&Config)-> ProductService{        ProductService{            config: config}}fnget_product(&self, id: i32)-> Product{ifself.config.debug_mode{println!("retrieving product for id: {:?}", id);}        Product}}impl<'a> BasketService<'a>{fnnew(config:&Config)-> BasketService{        BasketService{            config: config}}fnadd_product(&self, item: Product){ifself.config.debug_mode{println!("adding product {:?}", item);}}}fnmain(){let config= Config{ debug_mode:true};let product_service= ProductService::new(&config);let basket_service= BasketService::new(&config);let product= product_service.get_product(1);    basket_service.add_product(product);}

The rest of the code shouldn’t be too scary with the#[derive(Debug)] annotation being the only exception. For now let’s just accept that those are needed in order to print out the product with theprintln! macro.

The code is a bit more verbose than the JavaScript version which mostly boils down to the fact that JavaScript isn’t strongly typed. I still find the Rust code quite expressive and terse if we consider the benefits of safety, memory usage and performance.

Liked this Rust article?

I started learning Rust out of curiosity with zero experience in systems programming. I know the pain.Learning Rust doesn't have to be hard. If you liked the article, sign up here and I'll inform you about new Rust content. ✌🏼

Written by Author

Christoph Burgdorf