Getting Started


In this tutorial, we will walk through the steps of building a simple Rails app called Flipper. It is essentially a simplified version of the Textify demo that allows you to flip a piece of text upside down:

Flipper

To make things interesting, we will be implementing the core functionality in Rust using Helix. At the end of the tutorial we will also cover deploying this app to Heroku.

Step 0: Install Rust

Before we begin, we need to install Rust using the rustup installer:

$ curl https://sh.rustup.rs -sSf | sh

If you already have rustup installed, run this command to ensure you have the latest version of Rust:

$ rustup update

Step 1: Create a new Rails project

First, we’ll need a new rails project. (If you are integrating Helix into an existing Rails project, you may skip this step.)

$ rails new --skip-active-record flipper

Since we are not going to need a database for this simple app, we can simplify things by removing Active Record with the --skip-active-record flag. To make sure things are working properly, let’s make sure we can run the Rails server:

$ bin/rails server

If you visit http://localhost:3000 in your browser, you should be greeted by a page similar to this:

Yay rails

Once you have verified that everything is working, exit the Rails server by pressing Ctrl+C.

Step 2: Generate a Helix crate

To start using Helix, add the helix-rails gem to your Gemfile:

source 'https://rubygems.org'

# ...

gem 'helix-rails', '~> 0.5.0'

Be sure to run bundle install afterwards.

Now that we have Helix installed, we can generate a Helix crate:

$ rails generate helix:crate text_transform

This will generate a Helix crate called text_transform, located in crates/text_transform. A Helix crate is simultaneously a Rust crate and a Ruby gem. This encourages you to structure your Rust code as a self-contained library separate from your application code.

Looking at the boilerplate generated by Helix, we can see that it generated a Rust file for us:

#[macro_use]
extern crate helix;

ruby! {
    class TextTransform {
        def hello() {
            println!("Hello from text_transform!");
        }
    }
}

This defines a simple Ruby class TextTransform with a single class method. To test this out, we can run rake irb, which automatically compiles the Rust code and puts us into an irb session:

$ rake irb
>> TextTransform.hello
Hello from text_transform!
=> nil

As you can see, we were able to invoke the method (implemented in Rust) from Ruby. Pretty cool!

Step 3: Implement the text_transform library

Now that we have the boilerplate down, let’s implement the text_transform library.

Let’s begin by writing some tests using RSpec.

First we will add rspec as development dependency:

Gem::Specification.new do |s|
  s.name = 'text_transform'
  # ...
  s.add_development_dependency 'rspec', '~> 3.6'
end

Be sure to run bundle install afterwards.

Then we will add our test:

require "text_transform"

describe "TextTransform" do
  it "can flip text" do
    expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")
  end

  it "can flip the text back" do
    expect(TextTransform.flip("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")).to eq("Hello Aaron (@tenderlove)!")
  end
end

As expected, the tests will fail as we have not implemented the flip method:

$ rspec
FF

Failures:

  1) TextTransform can flip text
     Failure/Error: expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")

     NoMethodError:
       undefined method `flip' for TextTransform:Class
     # ./spec/text_transform_spec.rb:5:in `block (2 levels) in <top (required)>'

  2) TextTransform can flip the text back
     Failure/Error: expect(TextTransform.flip("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")).to eq("Hello Aaron (@tenderlove)!")

     NoMethodError:
       undefined method `flip' for TextTransform:Class
     # ./spec/text_transform_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.00068 seconds (files took 0.13472 seconds to load)
2 examples, 2 failures

Now that we have some failing tests, let’s implement the missing method (in Rust!):

#[macro_use]
extern crate helix;

ruby! {
    class TextTransform {
        def flip(text: String) -> String {
            text.chars().rev().map(|char| {
                match char {
                    '!' => '¡', '"' => '„', '&' => '⅋', '\'' => '‚', '(' => ')', ')' => '(', ',' => '‘', '.' => '˙',
                    '1' => 'Ɩ', '2' => 'ᄅ', '3' => 'Ɛ', '4' => 'ㄣ', '5' => 'ϛ', '6' => '9', '7' => 'ㄥ',
                    '9' => '6', ';' => '؛', '<' => '>', '>' => '<', '?' => '¿',
                    'A' => '∀', 'B' => '𐐒', 'C' => 'Ↄ', 'D' => '◖', 'E' => 'Ǝ', 'F' => 'Ⅎ', 'G' => '⅁',
                    'J' => 'ſ', 'K' => 'ʞ', 'L' => '⅂', 'M' => 'W',
                    'P' => 'Ԁ', 'Q' => 'Ό', 'R' => 'ᴚ', 'T' => '⊥', 'U' => '∩', 'V' => 'ᴧ', 'W' => 'M',
                    'Y' => '⅄', '[' => ']', ']' => '[', '^' => 'v', '_' => '‾',
                    '`' => ',', 'a' => 'ɐ', 'b' => 'q', 'c' => 'ɔ', 'd' => 'p', 'e' => 'ǝ', 'f' => 'ɟ', 'g' => 'ƃ',
                    'h' => 'ɥ', 'i' => 'ᴉ', 'j' => 'ɾ', 'k' => 'ʞ', 'm' => 'ɯ', 'n' => 'u',
                    'p' => 'd', 'q' => 'b', 'r' => 'ɹ', 't' => 'ʇ', 'u' => 'n', 'v' => 'ʌ', 'w' => 'ʍ',
                    'y' => 'ʎ', '{' => '}', '}' => '{',

                    // Flip back
                    '¡' => '!', '„' => '"', '⅋' => '&', '‚' => '\'', '‘' => ',', '˙' => '.',
                    'Ɩ' => '1', 'ᄅ' => '2', 'Ɛ' => '3', 'ㄣ' => '4', 'ϛ' => '5', 'ㄥ' => '7',
                    '؛' => ';', '¿' => '?',
                    '∀' => 'A', '𐐒' => 'B', 'Ↄ' => 'C', '◖' => 'D', 'Ǝ' => 'E', 'Ⅎ' => 'F', '⅁' => 'G',
                    'ſ' => 'J', '⅂' => 'L',
                    'Ԁ' => 'P', 'Ό' => 'Q', 'ᴚ' => 'R', '⊥' => 'T', '∩' => 'U', 'ᴧ' => 'V',
                    '⅄' => 'Y', '‾' => '_',
                    'ɐ' => 'a', 'ɔ' => 'c', 'ǝ' => 'e', 'ɟ' => 'f', 'ƃ' => 'g',
                    'ɥ' => 'h', 'ᴉ' => 'i', 'ɾ' => 'j', 'ʞ' => 'k', 'ɯ' => 'm',
                    'ɹ' => 'r', 'ʇ' => 't', 'ʌ' => 'v', 'ʍ' => 'w','ʎ' => 'y',

                    _ => char,
                }
            }).collect()
        }
    }
}

The flip method takes a string as input, splits it into characters, maps each character into its “upside down lookalike” and joins them back up into a new string.

If you look at the code, you’ll notice that we’re using a lot of high-level features here such as iterators and blocks. Now this might sound suboptimal, but the Rust compiler will be able to see through all of that and generate highly-optimized machine code that could even outperform your carefully hand-written loop.

Now that we have implemented the method, let’s run the tests again:

$ rspec
FF

Failures:

  1) TextTransform can flip text
     Failure/Error: expect(TextTransform.flip("Hello Aaron (@tenderlove)!")).to eq("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")

     NoMethodError:
       undefined method `flip' for TextTransform:Class
     # ./spec/text_transform_spec.rb:5:in `block (2 levels) in <top (required)>'

  2) TextTransform can flip the text back
     Failure/Error: expect(TextTransform.flip("¡(ǝʌolɹǝpuǝʇ@) uoɹɐ∀ ollǝH")).to eq("Hello Aaron (@tenderlove)!")

     NoMethodError:
       undefined method `flip' for TextTransform:Class
     # ./spec/text_transform_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.00068 seconds (files took 0.13472 seconds to load)
2 examples, 2 failures

Hmm, it is not seeing the flip method we just implemented. This is because since Rust is a compiled-language, we would have to re-compile our code after making any changes:

$ rake build
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
   Compiling text_transform v0.1.0 (file:///private/tmp/flipper/crates/text_transform)
    Finished release [optimized] target(s) in 0.95 secs

Now if we run the tests again, everything will work as expected:

$ rspec
..

Finished in 0.00348 seconds (files took 0.12317 seconds to load)
2 examples, 0 failures

Step 4: Adding a feature

To avoid needing to manually recompile, we can wrap this in a rake task and make rake build its dependency:

require 'bundler/setup'
require 'rspec/core/rake_task'
import 'lib/tasks/helix_runtime.rake'

RSpec::Core::RakeTask.new(:spec) do |t|
  t.verbose = false
end

task :spec => :build
task :default => :spec

The trick is to make rake build a dependency of your spec task. That way, running rake spec will always ensure the Rust code is built (and up-to-date) before running your tests, just like the built-in rake irb task.

To show you that workflow, let’s try to add a new feature.

require "text_transform"

describe "TextTransform" do
  # it "can flip text" ...

  # it "can flip the text back" ...

  it "can flip table" do
    expect(TextTransform.flip("┬──┬ ノ( ゜-゜ノ)")).to eq("(╯°□°)╯︵ ┻━┻")
  end

  it "can flip the table back" do
    expect(TextTransform.flip("(╯°□°)╯︵ ┻━┻")).to eq("┬──┬ ノ( ゜-゜ノ)")
  end
end

As you can see, this is a pretty simple feature: if you give a table, it’ll flip it; if you give it a flipped table, it’ll flip it back.

So now we can try running our test again with rake spec, and they’re failing as expected.

$ rake spec
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
   Compiling text_transform v0.1.0 (file:///~/code/flipper/crates/text_transform)
    Finished release [optimized] target(s) in 1.7 secs
..FF

Failures:

  1) TextTransform can flip table
     Failure/Error: expect(TextTransform.flip("┬──┬ ノ( ゜-゜ノ)")).to eq("(╯°□°)╯︵ ┻━┻")

       expected: "(╯°□°)╯︵ ┻━┻"
            got: "(ノ゜-゜ )ノ ┬──┬"

       (compared using ==)
     # ./spec/text_transform_spec.rb:13:in `block (2 levels) in <top (required)>'

  2) TextTransform can flip the table back
     Failure/Error: expect(TextTransform.flip("(╯°□°)╯︵ ┻━┻")).to eq("┬──┬ ノ( ゜-゜ノ)")

       expected: "┬──┬ ノ( ゜-゜ノ)"
            got: "┻━┻ ︵╯)°□°╯)"

       (compared using ==)
     # ./spec/text_transform_spec.rb:17:in `block (2 levels) in <top (required)>'

Finished in 0.02586 seconds (files took 0.14297 seconds to load)
4 examples, 2 failures

Failed examples:

rspec ./spec/text_transform_spec.rb:12 # TextTransform can flip table
rspec ./spec/text_transform_spec.rb:16 # TextTransform can flip the table back

With the tests in place, we can go ahead and implement our feature. This is going to be pretty straightforward; we’re just going to have a conditional at the top to check for the special cases.

#[macro_use]
extern crate helix;

ruby! {
    class TextTransform {
        def flip(text: String) -> String {
            if text == "┬──┬ ノ( ゜-゜ノ)" {
                return "(╯°□°)╯︵ ┻━┻".to_string();
            } else if text == "(╯°□°)╯︵ ┻━┻" {
                return "┬──┬ ノ( ゜-゜ノ)".to_string();
            }

            // ...
        }
    }
}

Going back to the terminal, you can see that by running “rake spec”, it automatically rebuilds our native extension. Therefore, everything Just Worked™.

$ rake spec
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
   Compiling text_transform v0.1.0 (file:///~/code/flipper/crates/text_transform)
    Finished release [optimized] target(s) in 1.9 secs
....

Finished in 0.00363 seconds (files took 0.13734 seconds to load)
4 examples, 0 failures

Step 5: Putting it all together

Now that we have built a library to do the heavily-lifting for us, we wire everything up inside our Rails app.

First let’s create the route:

Rails.application.routes.draw do
  resources :flips, path: '/', only: [:index, :create]
end

Then we will create the controller:

class FlipsController < ApplicationController
  def index
    @text = params[:text] || "Hello world!"
  end

  def create
    @text = TextTransform.flip(params[:text])
    render :index
  end
end

And finally the template:

<h1>Flipper</h1>

<%= form_tag do %>
  <%= text_field_tag :text, @text %>
  <%= submit_tag "Flip!" %>
<% end %>

After starting the Rails server with the bin/rails server command, you should have a working Flipper app waiting for you at http://localhost:3000:

Flipper

As you can see, with pretty minimal effort, we were able to create a Ruby native extension written in Rust using Helix, and integrate it into our Rails app.

Step 6: Deploy to Heroku

Finally, we will deploy our Flipper app to Heroku.

First, you will need to create a Heroku account and install the Heroku CLI tools.

Then, we will need to create a Heroku app:

$ heroku create

Since Flipper is both a Ruby and a Rust app, we will need to set up the buildpacks manually:

$ heroku buildpacks:add https://github.com/hone/heroku-buildpack-rust
$ heroku buildpacks:add heroku/ruby

These commands add the Rust buildpack, which makes the Rust compiler available, as well as the regular Ruby buildpack that knows how to configure a Rails app.

Finally, we can deploy the app to Heroku:

$ git push heroku master

With that, you should have a working Flipper app – powered by Rust, running inside a Rails app – up and running on the Internet. Congratulations!

Further reading