Getting Started
- Step 0: Install Rust
- Step 1: Create a new Rails project
- Step 2: Generate a Helix crate
- Step 3: Implement the
text_transform
library - Step 4: Adding a feature
- Step 5: Putting it all together
- Step 6: Deploy to Heroku
- Further reading
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:
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:
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
from crates/text_transform
, 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:
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!