actix – an actor framework for the Rust programming language

anchorRust

What is Rust?

Aside from being "an iron oxide" according to Wikipedia, Rust is also the name of a programming language that was originally invented by Graydon Hoare back in 2006. You can think about it as an interesting mix of high-level languages like JavaScript or Ruby and low-level high-performance languages like C++.

Similar to C++ it is a compiled language without a garbage collector. The fact that it usually uses LLVM to compile to machine code means that a lot of the optimizations that were developed to compile C/C++ code can also be applied to Rust programs.

Similar to JavaScript or Python the language often feels more high-level than C/C++, and it has a built-in dependency manager called cargo. This is somewhat similar to npm in JavaScript or bundler in Ruby, with the main difference that cargo is also used to build and test your applications and libraries.

The main advantage over all the other languages is safety. The Rust compiler is often quite strict on what data you are allowed to access at what point in the application logic, because it knows about concepts like threads and potential race conditions. This might not seem relevant to JavaScript developers, but even there with nested callbacks you might easily run into a situation where the thing you're trying to access has been deleted already. This issue is impossible with Rust.

Why would you use it? It is very fast, it can be embedded into scripting languages if raw speed is needed, it can compile to WebAssembly, and most of all, it is much safer than C++, which would also be a candidate for all the previous points.

How to get started? Follow the instructions at rustup.rs

anchorActors

The "actor model" is the main primitive that powers the Erlang programming language and its descendant, [Elixir]. It describes a programming model that simplifies the development of concurrent and multi-threaded applications or even applications that run distributed on multiple machines.

An actor is a thing that can only be interacted with using "messages". A message can basically be anything that the actor can understand and in response to a message an actor is allowed to do several things, including:

  • send a response
  • send messages to other actors
  • change its own state

Let's look at a simplified example in JavaScript syntax:


class CounterActor {
  constructor() {
    this.count = 0;
  }

  onReceive(message) {
    if (message.type === 'plus-one') {
      this.count += 1;
    }

    return this.count;
  }
}

The CounterActor class in this example is initialized with an internal state called count that is set to zero and it responds to plus-one messages by increasing the count state and returning the new value.

The complexity of actors is relatively low, and that is because the complexity is usually hidden in the actor frameworks that are used to run these types of primitives in the end. One example of such an actor framework is [actix], which we will have a closer look at now.

anchoractix

[actix] is the low-level actor framework that powers actix-web, a high-performance web framework for the [Rust][rust] programming language.

While actix-web is interesting and worth another blog post, we will focus on the low-level primitive [actix][actix] for now as it is vital to understanding the higher level concepts.

To get started with actix, let's port our CounterActor above to Rust:

use actix::prelude::*;

// `PlusOne` message implementation

struct PlusOne;

impl Message for PlusOne {
    type Result = u32;
}

// `CounterActor` implementation

struct CounterActor {
    count: u32,
}

impl CounterActor {
    pub fn new() -> CounterActor {
        CounterActor { count: 0 }
    }
}

impl Actor for CounterActor {
    type Context = Context<Self>;
}

impl Handler<PlusOne> for CounterActor {
    type Result = u32;

    fn handle(&mut self, _msg: PlusOne, _ctx: &mut Context<Self>) -> u32 {
        self.count += 1;
        self.count
    }
}

Since Rust is a typed language all structures need to be declared upfront. In the snippet above we first import all the necessary things from the actix::prelude module, and then we define how a PlusOne message should look like. In the JavaScript implementation the message had a type property, but since we have a strict type system available in Rust there is no need to explicitly declare that. That leaves us with an empty PlusOne message, indicated by the struct PlusOne which does not have any content. The message does have a Result type though, defined by type Result = u32; which means "unsigned 32 bit integer".

The CounterActor implementation is another struct which is roughly similar to a class in JavaScript. It does implement several "traits", which is very roughly what are called "interfaces" in e.g. TypeScript or Java.

The Actor trait defines that CounterActor is in fact an actor that complies with the necessary interface to be run by the actix framework. The Context type declaration can be used for more advanced implementations, but for now we can use the default implementation that is provided by actix itself.

Finally we implement the Handler trait for the PlusOne message that we defined earlier. In the handle() method we increment the count state and then return the new value to tell actix that this is our response to the message.

anchorRunning our CounterActor

While building the actor was relatively easy, running it is unfortunately still a little hard while Rust figures out its version of async/await (see futures-await).

The following code will startup our actor, send a message, wait for the response, send another message, wait for the response and finally exit the application:

let sys = actix::System::new("test");

let counter: Addr<Syn, _> = Arbiter::start(|_| CounterActor::new());
let counter_addr_copy = counter.clone();

let result = counter.send(PlusOne)
    .and_then(move |count| {
        println!("Count: {}", count);
        counter_addr_copy.send(PlusOne)
    })
    .map(|count| {
        println!("Count: {}", count);
        Arbiter::system().do_send(actix::msgs::SystemExit(0));
    })
    .map_err(|error| {
        println!("An error occured: {}", error);
    });

Arbiter::handle().spawn(result);

sys.run();

The first thing to do when using actix actors is to set up a System, that handles all those actor interactions for us. We do so by calling actix::System::new() and passing it a name.

Next we start an [Arbiter][artiter] in a new thread, that runs our CounterActor. If that sounds like a foreign language to you, don't worry, I had the same feeling at first. For now all you need to know is that Syn means that it is running in a separate thread, and that the Arbiter is the thing that controls that thread.

The Arbiter::start() call returns an Addr (short for address), that we can use to talk to the actor. The Addr struct has methods like send() that can be used to send messages to the actor and receive their responses.

Rust is very strict around data ownership, and the "borrow checker" makes sure that data access can only happen in safe ways. Since we use the counter variable for the first send() call, we are (at least currently) not allowed to reuse it inside the callback. Instead we need to create a clone() and use that one instead.

The large code structure in the middle of the snippet looks roughly like a Promise-chain in JavaScript, and it is exactly that. The counter.send() call returns what Rust calls a Future. A Future (like a Promise) has several methods that can be used to assemble a sort of pipeline of how to handle the result that the Future will at some point return.

In this specific case we use .and_then() to wait for the result of the PlusOne message, then print it out to the console, and then fire off another PlusOne message. Once that second message has returned we print the response again and then use a special system arbiter call to exit the process.

The major difference between a Promise in JavaScript and a Future in Rust is that a Promise automatically runs when it is created, but a Future needs to be started explicitly. This difference exists for performance reasons, and because in JavaScript there is no such thing as running on different threads.

To start the Future that we have assembled we use the Arbiter::handle().spawn() function, and then finally start the System once everything is wired up correctly to block the current thread until all actors have finished running.

anchorSummary

This blog post covered some of the basic concepts of writing actors using the actix framework for Rust. In a follow-up post we will look into writing a small TCP client using these primitives, which can for example be used to forward traffic to websocket clients or just log the received messages to the console.

Grow your business with us

Our experts are ready to guide you through your next big move. Let us know how we can help.
Get in touch