Mixing Ruby and Rust on Heroku

Written by: Daniel P. Clark
6 min read

Ruby has been around for a good many years now and has become a quite well-seasoned language. There are many profitable tools, projects, and sites with code written in Ruby, which is a nice thing that we can utilize even in Rust.

In this post, I'll be covering running Ruby code from Rust, touch on Rust from Ruby, and deploying each on a Heroku instance. The Rust language can benefit from having Ruby's ecosystem accessible and Ruby to have Rust's performance.

Let's have a look at how we can go about doing this for ourselves.

Ruby on Rust

This guide assumes you have Ruby, Rust, git, and Heroku's CLI tool installed.

First you need to create your Rust project with the following:

cargo new ruby_on_rust --bin
cd ruby_on_rust/

Next we need to prove we can Ruby code on Rust. For this, we'll use ruru as they allow us to run the Ruby Virtual Machine from Rust. So add the ruru dependency to your Cargo.toml file.

[dependencies]
ruru = "~0.9"

Now we need a simple piece of code to try to prove we're calling Ruby code. For this example, we'll create a Ruby string and call the String#upcase method. We'll convert the result back from a Ruby string to a Rust string and test that we have the desired result.

Before we do that, ruru, at this time, requires that the Ruby library be among our build dependencies. To accommodate this, we'll create a Rakefile task to copy our lib file to target/debug/deps.

# Rakefile
require 'rake/testtask'
require 'fileutils'
desc 'prep'
task :prep do
  puts 'Copying rubylib to build deps…'
  `mkdir -p target/debug/deps`
  `cp $(ruby -e \
  'puts File.join(RbConfig::CONFIG["libdir"], \
  RbConfig::CONFIG["LIBRUBY_ALIASES"].split(" ").first)') \
  target/debug/deps`
end
desc 'test run'
  task run: :prep do
  exec 'cargo run'
end
task default: :prep

We've thrown in the run command to call prep before using cargo run, which, once we've implemented our web server, will run our web server.

And now to run Ruby code from Rust.

// src/main.rs
extern crate ruru;
use ruru::{VM, RString, Object};
fn ruby_upcase(s: &str) -> String {
  RString::new(s).
    send("upcase", vec![]).
    try_convert_to::<RString>().
    unwrap_or(RString::new("")).
    to_string()
}
#[test]
fn it_works() {
  VM::init();
  assert_eq!(
    ruby_upcase("Hello World!"),
    "HELLO WORLD!".to_string()
  );
}
fn main() {}

At this point, we run cargo test and we get a passing result. We have successfully called Ruby's String#upcase method and received the reward of integrating Ruby into Rust.

An added advantage of having ruru init the Ruby VM is that any Ruby objects we create should properly have their memory freed without issue.

Since Rust is a typed language and Ruby isn't, a method call in Ruby can return any type of object. For that, ruru has methods and procs return an AnyObject type. From this we try the try_convert_to::<T>() method to get a Result<T> for our expected type.

In the example above, the use of unwrap_or(RString::new"") will give us the Ruby string back if that's what the method returns, or it will create a blank Ruby string if we didn't get a Ruby string back. So no matter what item is returned in our result, we're guaranteed an RString to work with, which we can call to_string() on to get a Rust String.

Rocket web framework

Next, we'll write a minimal website with Rocket and display our resulting Ruby code on the web.

First, add Rocket to the Cargo.toml file.

[dependencies]
ruru = "~0.9"
rocket = "0.3.3"
rocket_codegen = "0.3.3"

And now we copy the getting started example from rocket.rs and call our method in it.

// src/main.rs
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
//
// Our ruru code…
//
#[get("/")]
fn index() -> String {
  ruby_upcase("Hello World!")
}
fn main() {
  VM::init();
  rocket::ignite().
    mount("/", routes![index]).
    launch();
}

Be sure to use the nightly version of Rust for Rocket.

Next, we run our rake command, rake run, and our website will take a little while to build. Once it's done building, a local web server will be running, and it will give you a URL to try out on your machine to see your site.

Go ahead and open that in your browser. At this point, you will see the page load with our upcased “HELLO WORLD!” and we'll know that it works locally.

!Sign up for a free Codeship Account

Deploying to Heroku

First off, we'll run the heroku create command with a Heroku buildpack for our language to the one designed for a Rust server to create a server instance for this project with your Heroku client. Since we're also using Ruby, we'll need to add a buildpack for that as well.

heroku create --buildpack https://github.com/emk/heroku-buildpack-rust.git
heroku buildpacks:add heroku/ruby

Before we can push this to Heroku, we need to set it so that Heroku will use the nightly release of Rust, as that is a requirement for Rocket.

echo "VERSION=nightly" > RustConfig

We need to set what Heroku should run for our web server with the Procfile.

echo "web: ROCKET_PORT=\$PORT ROCKET_ENV=prod ./target/release/ruby_on_rust" > Procfile

And the Ruby buildpack for Heroku will fail to run unless we have the Gemfile and Gemfile.lock files, so let's add that.

touch Gemfile
bundle

And now we're ready to deploy.

git add .
git commit -m "Initial ruru on Rocket build."
git push heroku master

At this point, you should have enough time to make a cup of coffee while you wait for it to build. Once it's done, you can type heroku open for it to open the running site in your browser. And you've done it!

If you named your project something else, be sure to change the names in all places. If the Procfile doesn't point to the right executable, your site won't run.

Rust on Ruby with Helix

Helix has a pretty awesome generator system that generates code for you in a Rails project so you can drop some Rust code into your program with very little effort. They've written up a getting started web page that does a great job giving you all the details for getting a Rails site to run Rust code pretty quickly.

The one thing their How To is lacking is the database issue where Heroku won't accept the default sqlite3 database -- you'll need to do your own research for changing the database settings to work on Heroku before the test example will work for you.

Helix's generator will create a Rust crate each time you use the generator tool and place it in a crates subdirectory. It's nice that each of these are individually namespaced for specific purposes and included in your Rails website like native Ruby code. You may even write Ruby-like Rust code with their very advanced Rust macros that do a lot of work parsing Ruby style coding into Rust equivalents.

Here's an example of what turning a Ruby string to uppercase through Rust is like.

#[macro_use]
extern crate helix;
ruby! {
  class TextTransform {
    def upcase(text: String) -> String {
      text.to_uppercase()
    }
  }
}

And anywhere in your Rails project, you can simply call TextTransform.upcase("my string") to get your result back: "MY STRING".

Using Helix for adding Rust in Rails is a no brainer for efficiency and convenience.

Summary

There is a vast number of tools in Ruby that are hard to work without: database abstractions, object mappers, many web utilities, and the list goes on. You can keep most of the time efficiency of developing in Ruby while in Rust if you simply use Ruby in Rust. And system-heavy code in Ruby is not so much of an issue anymore, as you can get a rough equivalent of C code performance without many of the pitfalls of C by calling into Rust from Ruby.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.