HomeBlogJVM 2 Rust – A brief introduction to Rust for Java developers
James Daly

JVM 2 Rust – A brief introduction to Rust for Java developers

rust language

This post is aimed at Java or Kotlin backend developers who wish to start writing web services in Rust or are simply interested in other languages. Below I will explain what Rust is and show you how to build a basic web service in Rust.

Author: James Daly

Rust Compared To Java

Rust is a systems programming language. This means that Rust is designed to give the developer close access to the machine that the code is running on, similar to C++. Java on the other hand is an object-oriented language in which the developer must communicate with the JVM which in turn translates Java Byte code into machine code. Rust code can be compiled directly to machine code. This allows developers like you and me to write more performant code. This is Rust’s main selling point as a backend language.

Basic Tooling in Rust

One of my favorite things about developing in Java is the amazing suite of tooling available. The tooling in Rust is not quite as good yet, but it’s improving every day. In Java, we use the Javac to compile our programs. In Rust, we use Rustc. To install Rustc simply run this command:


curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

In the JVM space we usually use something like Maven or Gradle to manage our dependencies and builds. The Rust equivalent for this is Cargo. Cargo is a core part of the Rust development experience. When you ran the previous command, it also installed Cargo. To verify that cargo has been installed correctly please run:

cargo version

Cargo is what you will use to build your application, manage your dependencies and run your tests.

Setting up a project in Rust

To start off let’s use Cargo to generate a shell project.


cargo new rustApp

This should have created a new directory called “rustApp”. In this directory, you will find a simple “hello world” program. Change into this directory and run:


cargo run

If everything has gone well you should have a `hello world!` in your terminal.

Comparing Languages: Java vs Rust

If you navigate to the main.rs file you will find our hello world code in rust:

fn main() {
  println!("Hello, world!");
}

Here is the equivalent code in java:

package nl.codenomads.javaApp;

public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

As you can see our Java and Rust are not so different. The Java code needs to be wrapped in a class as Java is an Object Orientated language but other than that they are very similar. Both languages require a main function where the application will start and both have built-in tools for printing to the console. In Java, we use a static method whilst in Rust we use something called a macro.  (This is beyond the scope of this post, but you can take a look here for why is println! a macro?.

Let’s Get Web

For this post, I will use Actix-Web for a Rust web framework. To add Actix Web to your rust project simply run this command from the base directory:


cargo add actix-web

This will add the newest actix-web version to your Cargo.toml file. Take a look inside:


[package]

name = "rustApp"

version = "0.1.0"

authors = ["James Daly <james@codenomads.nl>"]

edition = "2018"



# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html



[dependencies]

actix-web = "3.3.2"

Your Cargo.toml in rust is like a pom.xml file in Java. It is where we keep all our dependencies and meta information about the project. You may have noticed a new file appeared called Cargo.lock. This file contains more exact information about your dependencies and transient dependencies. You should almost never edit this file manually.

Lift Off

Now that we’ve added Actix-Web to our project we can create our first backend service. Let’s change our main function to spin up a server instance:


use actix_web::{App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new())
        .bind("127.0.0.1:8080")?
        .run()
        .await
}

On the first line, you can see our imports. To create a basic server we need to import App. This is an application builder. It follows the builder pattern and gives us a clean interface to build our web application on start-up. The application will contain the services and any global state we may need. Our other import is HttpServer. This will serve our application at runtime.

On the third line, you can see something called a macro. This can be thought of similarly to Spring annotations. Unlike Spring annotations, all macros only exist at compile time. They provide a mechanism for the compiler to generate or modify code. If you ever want to see what a macro is doing you can run:


cargo expand

Now you can see exactly what this macro is doing to your code. As someone who has invested a lot of time into finding bugs caused by annotations in Java, this is one of my favorite features in Rust.

fn main() -> std::io::Result<()> {
    actix_web::rt::System::new("main").block_on(async move {
        {
            HttpServer::new(|| App::new())
                .bind("127.0.0.1:8080")?
                .run()
                .await
        }
    })
}

In this instance, it is simply taking the code from inside your main function and running it in an asynchronous runtime called Tokio. This allows your application to handle requests in parallel. It also addresses the Rust constraint that your main function cannot be asynchronous.

If we keep moving down you’ll see `HttpServer::new()`. This is a constructor for our HttpServer. This takes an application factory as a parameter. The next thing we do is create an application factory using the App builder. On the next line, we simply bind out the application to a port. Then we hit run. Run is an asynchronous function that if left alone will run forever. It will only stop given a failure or if it is sent an interrupt to end gracefully because it is an asynchronous call we must append await to the end. If your application need’s to do any cleanup after it runs, that cleanup should happen after await has been called.

As you can see this isn’t so different to Java:

package nl.codenomads.javaApp;;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

}

In Java, we annotate our class similar to how we add a macro to our main function in Rust. Then, once inside the main function we create an instance of our application. We use fewer lines of code to get our application running, but the downside is that it can be difficult to tell exactly what Spring is doing here. The only way to get a good idea of what is happening here is to read documentation or step throw the application during start-up with a debugger.

A Response

With Actix Web it is quite simple to create an endpoint and bind it to a path. There is a library of functions, structs and macros waiting for you to make your job simpler. Let’s add some of them to our main.rs file:


use actix_web::{get, App, HttpServer};

HttpServer and App were already covered in the previous section, so I will skip those. The new import here is `get`. This is a macro that you can use to create a get endpoint. It will generate the code for a get path and pass information from the socket to your desired function. Let’s take a look at that function:


#[get("/index.html")]
async fn index() -> String {
    String::from("Hello World!")
}

Here you can see Actix Web’s `get` macro. This transforms our function into a service at compile time. To create this service it must be passed a path and the function must be marked as asynchronous. This service will act as a GET endpoint. Our endpoint is very basic, it takes no parameters and returns a simple string. (Remember, if you’d like to take a closer look at what happens at compile time you can run `cargo expand`) Thanks to Actix Web it is very simple to create clear and concise endpoints with little syntactic sugar.

If you try to run your application and hit this endpoint though you’ll receive a 404. Now, why is that? You have created an HttpServer that listens on port 8080 and serves your application. You have created a service but this service must be added to your application.

HttpServer::new(|| App::new().service(index))
        .bind("127.0.0.1:8080")?
        .run()
        .await

Seeing as the application is following the builder pattern we can chain call methods to add services and other pieces of functionality to our application. When we add a service, we are adding our path to a list of routes in the application. At runtime our HttpServer will loop continuously, listening on port 8080. When it receives a request it will iterate over the routes in our application until it finds a match for the requested path. It will then call our function asynchronously which in turn will handle the request. Now try running:


cargo run

Now you should get back a response at http://localhost:8080/index.html

Closing

That’s it! A basic web service built in Rust. Now I wouldn’t run off and deploy this in production quite yet unless your product owner is desperate to have a hello world page, but I hope this post has highlighted how relatively simple it is to create a backend in Rust. If you would like to know more about building web applications in Rust I would highly recommend Zero To Production In Rust by Luca Palmieri. Alternatively, if you would like to just learn more about Rust you should check out the free Rust Book.

James Daly

James Daly

Software Engineer