class: middle, center, overlay # Rust and Ruby ### An introduction to becoming Fire Mario
??? - Don't bother with who you are. - Let's get right into it. - First: - We're going to gloss over a number of things and we're really only going to touch on Rust as a language. At the end of the workshop I'll provide a number of links and resources for learning about Rust and getting involved in the community. - Second: - I'm by no means an expert. I've been using Rust for a little over a year or since Rust turned to 1.0. I've written a few web services in it, as well as an string library that we'll be using today. --- class: middle ## What is Rust? - Developed by Mozilla - Systems (general purpose) programming - Compiled - Statically typed - No garbage collection - Functional and imperative-procedural paradigms - Similar in ways to C++ but with memory safety - The most ❤️ language 2016, 2017 ??? - Rust was developed in house at Mozilla - Generally thought of as a "systems" programming language like C or C++ - It's compiled using the Rust compiler and LLVM - It's statically typed, but allows for type inference. - This basically means you don't always need to tell the compiler what type something is. The compiler is `smart` enough to `know` what the type should be in most cases - Two years running on the Stack Overflow Developer Survey! --- class: middle ## Who uses Rust? ### Mozilla ### NPM ### Dropbox ### Canonical [Full list](https://www.rust-lang.org/en-US/friends.html) ??? - Obviously Mozilla is using it, but there are other big brands! - Mozilla for Firefox, and their upcoming browser Servo. - NPM talks about using Rust on the bikeshed podcast [here](http://bikeshed.fm/89) - Dropbox and Canonical are listed, as well as a number of other big names on the friends page. --- class: middle ## Why Rust?* - Zero-cost abstractions - Move semantics - Guaranteed memory safety - Threads without data races - Pattern matching - Type inference - Program fearlessly like Ruby but with real performance - ... ??? - We're going to skip over a number of these in the interest of time. We will however make use of the last point. --- class: middle ## Schedule 1. ◽️ Create* a RubyGem 2. Add some boilerplate for getting Rust into the project 3. Write some Ruby 4. Run some tests 5. Mix in some pre-written Rust 6. Write some of our own Rust 7. Run some tests and benchmarks 8. Wrap up --- class: middle ## Create a RubyGem ```shell git stash git checkout step1 ``` ??? - OK job done! I've already created the gem in this case. - We can take a quick peek into the bits that make up a RubyGem: - The majority of what you see in the root directory is created by bundler when you create a gem for the first time. --- class: middle ## Create a RubyGem #### Gemfile ```ruby source 'https://rubygems.org' # Specify your gem's dependencies in fireflower.gemspec gemspec ``` ??? - Let's take a look into the Gem then gemspec file - Notice that the Gemfile is mostly empty, but references the gemspec. - Maybe we should actually look in the gemspec --- class: middle ## Create a RubyGem #### fireflower.gemspec ```ruby #... Gem::Specification.new do |spec| # Some stuff about who wrote this etc spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_development_dependency "bundler", "~> 1.12" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.0" end ``` ??? - Nearly everything here is also generated and may or may not pull from your dotfiles for author and email. - If we want to add a new gem as a dependency of this gem we can do so at the bottom. - For our purposes it's not important to have an deep understanding of the structure of a gem. Anything that we might want to add is already added in later steps. --- class: middle ## Create a RubyGem #### spec/fireflower_spec.rb ```ruby require 'spec_helper' describe Fireflower do it 'has a version number' do expect(Fireflower::VERSION).not_to be nil end it 'does something useful' do expect(false).to eq(true) end end ``` ??? - Next up is spec - Everything here is generated code at the moment. Notice that fireflower_spec just checks that we have a version number and has a failing test. --- class: middle ## Create a RubyGem #### lib/fireflower.rb ```ruby require "fireflower/version" module Fireflower # Your code goes here... end ``` ??? - Now onto lib - Again, all generated code at the moment. This is exactly what you get by default in a gem. - Next up is actually setting up the gem for Rust. --- class: middle ## Schedule 1. ✅ Create* a RubyGem 2. ◽️ Add some boilerplate for getting Rust into the project 3. Write some Ruby 4. Run some tests 5. Mix in some pre-written Rust 6. Write some of our own Rust 7. Run some tests and benchmarks 8. Wrap up --- class: middle ## Add some boilerplate for getting Rust into the project ```shell git stash git checkout step2 ``` ??? - OK job done, again! I promise we'll write some code soon! - This is another good chance to have a look around the code! - [diff here](https://github.com/whatisinternet/fireflower/compare/step1...step2?diff=unified&name=step2) --- class: middle ## Cargo.toml ```toml [package] name="fireflower" version="0.1.0" publish=false [package.metadata.thermite] [lib] crate-type = ["dylib"] [dependencies] ruru = "0.9" ``` ??? - This is roughly your gemspec or Gemfile but for Rust! - First up is the package information, which is very basic in this case. For a more complex example have a look at the Cargo file for diesel.rs - Next is actually a specific annotation for thermite. It's required to build the project with thermite which is a rake task for generating Rust-based Ruby extensions - We then move onto defining the crate-type. Normally in Rust this field wouldn't be required unless you specifically need to specify. In this case we do for thermite and ruru (the other rust requirement) to function correctly. - And finally we're on to the actual dependencies! - What is ruru? - An easy* way to integrate Rust with Ruby using native Ruby Links: https://github.com/malept/thermite https://github.com/d-unseductable/ruru --- class: middle ## Rake and gemspec - Rake file and ext/Rakefile now have boilerplate code for [Thermite](https://github.com/malept/thermite) - `fireflower.gemspec` now has a runtime dependency! ??? - The code for these three files is directly from the thermite documentation. - In the rakefile we require in the thermite rake tasks and override default with the one from thermite - ext/Rakefile is very similar. - We tell thermite where to find our rust and where to find our ruby and thermite takes care of meshing them together! - The gemspec file additions are straightforward as well. - The most interesting part is the new `runtime` dependency. - We add thermite so that we can use its included rake tasks and glue code Links: https://github.com/malept/thermite https://github.com/d-unseductable/ruru --- class: middle ## src/lib.rs ```rust #![deny(warnings, unused_variables, unsafe_code, unused_extern_crates)] #[macro_use] extern crate ruru; ``` ??? - Our first real Rust code! - Good news? This is valid Rust - Bad news? It won't compile - Let's unpack why: - The first line is an attribute - Docs: https://doc.rust-lang.org/beta/book/attributes.html - It's a little special though. We're actually telling the Rust compiler to be even more strict than usual. - This is similar to Magic comments in Ruby - Some docs: http://idiosyncratic-ruby.com/58-magic-instructions.html - The idea is we want our Rust code to be clean, stable, and fast. These are some of the real `lints` that I use on my projects. At the minimum however I would recommend having the deny warnings flag always on. There are drawbacks but they're fairly minor in this case. - Thus our Rust code will fail to compile because we're not using ruru yet. - Next up is another attribute and then a line that is roughly speaking a require. - Without going into too much detail, macros are part of Rust meta programming. However, in Rust you need to be explicit about any macros that you're going to import. This attribute is required in this case as we plan to use the macros, however the attribute is only required if you plan to use them - We did it! We're on to writing code! --- class: middle ## Schedule 1. ✅ Create* a RubyGem 2. ✅ Add some boilerplate for getting Rust into the project 3. ◽️ Write some Ruby 4. ◽️ Run some tests 5. Mix in some pre-written Rust 6. Write some of our own Rust 7. Run some tests and benchmarks 8. Wrap up --- class: middle ## Write some Ruby ```shell git stash git checkout step3 ``` ??? - Finally you get to write some Ruby! --- class: middle ## Write some Ruby ```shell rspec ```
??? - I've taken the liberty of writing some very basic specs. - If we run them you'll find that we're failing. - In a minute we're going to make these tests pass as an exercise. --- class: middle ## Write some ruby - Time to actually write Ruby! - You may want to uncomment this from the gemspec ```ruby spec.add_dependency "activesupport", "~> 5.0.1" ``` - And this in lib/case.rb ```ruby require 'active_support/inflector' ``` - Then in vagrant run the following ```shell bundle install ``` ??? - This part is optional: If you feel that your can write an effective string to `snake_case` without ActiveSupport go for it! - Your actual code will live in lib/case.rb - Let's open that now. --- class: middle split-40 ## Now it's your turn! .column[ - Possibly useful docs - Google: _ActiveSupport::Inflector_ - To run the tests ```shell rspec ``` ] .column[ ![](http://i.giphy.com/qo5oCeOmj9voc.gif) ] ??? - Your actual code will live in lib/case.rb - In the spec folder we have a number of tests that are skipped. At the end none should be skipped and none should fail. - I have a timer for 5-10 minutes depending on how everyone is doing. --- class: middle ## Schedule 1. ✅ Create* a RubyGem 2. ✅ Add some boilerplate for getting Rust into the project 3. ✅ Write some Ruby 4. ✅ Run some tests 5. ◽️ Mix in some pre-written Rust 6. Write some of our own Rust 7. Run some tests and benchmarks 8. Wrap up --- class: middle ## Mix in some pre-written Rust ```shell git stash git checkout step3.1 bundle install ``` ??? - Now that you've all written some Ruby code, it's now time to mix in some Rust code! - I've elected for the activesupport way of making a string snake case and we'll go through the changes in the next few slides. --- class: middle ## Mix in some pre-written Rust #### cargo.toml ```diff diff --git a/Cargo.toml b/Cargo.toml --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ crate-type = ["dylib"] [dependencies] ruru = "0.9" +Inflector = "*" ``` ??? - Only one change here: - We're adding a string inflection library to add `to_snake_case` and `to_camel_case` - I wrote this library and it's available on github.com/whatisinternet/Inflector - In this case we're saying "I need Inflector and I don't care what version I use". - This is not generally a good idea - I'd push for specifying a version that you know works as we see with `ruru` - In this case however it's safe since we're just doing a demo --- class: middle ## Mix in some pre-written Rust #### fireflower.gemspec ```diff diff --git a/fireflower.gemspec b/fireflower.gemspec --- a/fireflower.gemspec +++ b/fireflower.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.12" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "rspec-benchmark", "~> 0.3.0" - # spec.add_dependency "activesupport", "~> 5.0.1" + spec.add_dependency "activesupport", "~> 5.0.1" end ``` ??? - Fairly straightforward again here. - We've added activesupport to provide the ruby inflections we're going to use - I've also added a rspec plugin for benchmark testing which will be useful for showing the performance gains seen by adding Rust --- class: middle ## Mix in some pre-written Rust #### lib/case.rb ```diff diff --git a/lib/case.rb b/lib/case.rb --- a/lib/case.rb +++ b/lib/case.rb @@ -1,3 +1,32 @@ -# require 'active_support/inflector' +require 'active_support/inflector' -class Case; end +require 'thermite/fiddle' + +class Case + def ruby_to_snake_case(non_snake_cased_string = "") + non_snake_cased_string.to_s.underscore + end + + def ruby_to_camel_case(non_camel_cased_string = "") + non_camel_cased_string.to_s.camelize + end ``` ??? - Nothing ground breaking yet. - You will notice that I've added an extra require statement at the top beyond the expected active_support/inflector. - This is to support bringing rust code in which we'll see more of in the next slide. - I've implemented the snake case and camel case ruby methods and as we'll see next I've also implemented the stubs for the Rust methods. --- class: middle ## Mix in some pre-written Rust #### lib/case.rb ```diff #... + def to_snake_case(non_snake_cased_string = "") + _to_snake_case(non_snake_cased_string.to_s) + end + + def to_camel_case(non_camel_cased_string = "") + _to_camel_case(non_camel_cased_string.to_s) + end + + private + + def _to_camel_case(non_camel_cased_string) + raise "NOT IMPLEMENTED ERROR" + end +end + +toplevel_dir = File.dirname(File.dirname(__FILE__)) +Thermite::Fiddle.load_module('init_fireflower', + cargo_project_path: toplevel_dir, + ruby_project_path: toplevel_dir) ``` ??? - Both `to_snake_case` and `to_camel_case` are going to use Rust. You'll probably notice that I've left `_to_camel_case` unimplemented. That's what we're going to implement - Next is some glue code from the thermite README - The first line is just some ruby to get the top level directory - The second line glues the rust code together, running init_fireflower from `src/lib.rs` which we'll see next. --- class: middle ## Mix in some pre-written Rust #### src/lib.rs ```diff +extern crate inflector; +use inflector::Inflector; ``` ??? - lib.rs! We're starting to see some Rust code in action! - Line 1 is very similar in effect to `require` in Ruby. At this point we've told Rust that we intend to use the inflector crate somewhere and the compiler will now error if we fail to use it as we have enabled the `unused_external_crates` warning - Line 2 is slightly more interesting. - Rust has the concept of [`traits`](https://doc.rust-lang.org/book/traits.html) - "A trait is a language feature that tells the Rust compiler about functionality a type must provide." -- Rust book - In this case we're going to be using inflectors::Inflector trait which implements a number of features on both of Rusts string types (&str and String). - let's have a look at the Rust documentation here as it give a concrete example. [here](https://doc.rust-lang.org/book/traits.html) --- class: middle ## Mix in some pre-written Rust #### src/lib.rs ```diff +use ruru::{Class, Object, RString}; ``` ??? - Now we're going to perform a qualified import of three Types from ruru which will help us talk to Ruby - Since Rust &str and String are not strictly compatible with Ruby strings ruru provides a way to ensure we can talk back and forth. - Also since Rust has no concept of Class or Object as it's not an object oriented language we import some glue from ruru again. --- class: middle ## Mix in some pre-written Rust #### src/lib.rs ```diff +methods!( + RString, + _itself, + + fn to_snake_case(test: RString) -> RString { + RString::new(&test.unwrap_or(RString::new("")).to_string().to_snake_case()) + } ``` ??? - Finally some real Rust code! - Starting from the inside out we can see a function definition for `to_snake_case` - Functions and methods are denoted by `fn` - By default unless a function is marked `pub` it is considered private to the module that it's in. In this case that's exactly what we want. - If we're coming from a ruby perspective everything is fine right up until the colon. (If you've written Crystal this might still be OK). - Rust is a strongly typed language - This means that we need to define a contract on the input and output that the function will produce. Note: There is no Void, Null, Nil in Rust. - The input in this case is an `RString` and our output is an `RString` - This makes sense since we're going to be working with Ruby strings - Also note that variables are by default immutable. If you wish to use mutable variables you have to explicitly define them as `mut`. See Rust book - The next line is a bit hairy so let's break it down a little bit. I'm going to kind of gloss over some of the details here to make things a little easier but if you're interested make sure you check out the Rust book and the ruru documentation for more details - `RString::new()` - We're going to output a new Ruby string. Notice that there is no explicit return statement here. Rust assumes that the last line is returned just like in ruby. However you are welcome to add additional return statements in other areas of your code as needed. - `&test` - We're taking a `reference` to the variable test. From the Rust book: "Using an ampersand in front of a value takes a reference to it." - We've come to a point that I'm going to gloss over a little bit, and that's `unwrap_or(RString::new(""))` - Since Ruby has a concept of `nil` and methods can pass `nil` as a valid parameter to essentially anything we have to account for it in Rust. What our `to_snake_case` method actually receives is an `Option` which is either a valid `RString` or `Nothing`. Since it could be `Nothing` Rust forces us to deal with it now. `unwrap_or` is some sugar that Rust provides to make it easy to provide a fallback when dealing with options. - The next two lines should be very obvious. We convert the RString to a Rust String type and then use Inflectors to_snake_case method. - We're going to skip over lines 2 and 3, and act like they don't exist - Finally we're at `methods!`. This is a `macro`, similar to metaprogramming in Ruby this allows Rusticans to write syntax extensions which allow us to concisely define common patterns. Like metaprogramming this should be used "as a last resort" to produce well abstracted concise code. -- paraphrased from https://doc.rust-lang.org/beta/book/macros.html - This macro is provided by ruru and will apply any required transformations to the `_itself` variable on line 3. --- class: middle ## Mix in some pre-written Rust #### src/lib.rs ```diff +#[no_mangle] +pub extern "C" fn init_fireflower() { + Class::from_existing("Case").define(|itself| { + itself.def("_to_snake_case", to_snake_case); + // itself.def("_to_camel_case", to_camel_case); + }); +} ``` ??? - This is the last part before it's up to you to implement to_camel_case - Starting top down: - First we tell the compiler that the method we're about to write should not be mangled to permit usage with FFI. - Now we define a public method with the `pub` keyword. - We define that the method will be externally available from our crate, and that the signature should be compatible with C. - Finally in the second line, we see a method that only produces side effects and has an arity of 0. - Next up is the DSL used by ruru for exporting classes and methods on those classes. Essentially this will export from an existing class called `Case` the function `_to_snake_case`. - Notice that define has a somewhat familiar syntax. `|itself| {` is analogous to the Ruby block signature `foo do |bar|`. - The last note here is that `//` defines a comment in Rust. --- class: middle ## Schedule 1. ✅ Create* a RubyGem 2. ✅ Add some boilerplate for getting Rust into the project 3. ✅ Write some Ruby 4. ✅ Run some tests 5. ✅ Mix in some pre-written Rust 6. ◽️ Write some of our own Rust 7. Run some tests and benchmarks 8. Wrap up --- class: middle split-40 ## Write some of our own Rust .column[ - Possibly useful docs - docs.rs/Inflector - docs.rs/ruru - doc.rust-lang.org/book/ - To run the tests ```shell bundle exec rake && rspec ``` ] .column[ ![](http://i.giphy.com/ACcXRXwUqJ6Ok.gif) ] ??? - Your actual code will live in lib/case.rb - In the spec folder we have a number of tests that are skipped. At the end none should be skipped and none should fail. - I have a timer for 5-10 minutes depending on how everyone is doing. --- class: middle ## Schedule 1. ✅ Create* a RubyGem 2. ✅ Add some boilerplate for getting Rust into the project 3. ✅ Write some Ruby 4. ✅ Run some tests 5. ✅ Mix in some pre-written Rust 6. ✅ Write some of our own Rust 7. ◽️ Run some tests and benchmarks 8. Wrap up --- ## Run some tests and benchmarks ```shell git stash git checkout step4 bundle install ``` ??? - We already have a bunch of tests. You might even notice that we have benchmark tests that specify how much faster our Rust implementation is vs the Ruby implementation. --- ## Run some tests and benchmarks ```Ruby #... @case.to_camel_case("foo_bar_is_a_string") }.to perform_faster_than(time: 1.4, warmup: 1.2) { @case.ruby_to_camel_case("foo_bar_is_a_string") }.at_least(12).times #... @case.to_snake_case("fooBarIsAString") }.to perform_faster_than(time: 2.4, warmup: 2.2) { @case.ruby_to_snake_case("fooBarIsAString") }.at_least(35).times #... ``` ??? - Snake case is about 30 times faster in Rust vs Ruby - Camel case is about 10 times faster than in Ruby --- ## Run some tests and benchmarks ## [Inflector issue #18](https://github.com/whatisinternet/inflector/issues/18) ??? - Obviously this is a trivial example as we're not really using the full power of Rust, and it is trivial to write Rust code that will perform worse than equal Ruby code. For an example see https://github.com/whatisinternet/inflector/issues/18 --- class: middle ## Schedule 1. ✅ Create* a RubyGem 2. ✅ Add some boilerplate for getting Rust into the project 3. ✅ Write some Ruby 4. ✅ Run some tests 5. ✅ Mix in some pre-written Rust 6. ✅ Write some of our own Rust 7. ✅ Run some tests and benchmarks 8. ◽️ Wrap up --- class: middle split-40 ## Wrap up and resources .column[ - Help - https://reddit.com/r/rust - `#rust` on irc.mozilla.org - https://users.rust-lang.org/ ] .column[ - Resources: - https://rust-lang.org - https://doc.rust-lang.org/book - https://docs.rs - http://www.newrustacean.com/ - https://crates.io ] ??? - Resources: - The Rust book - The best guide to learning Rust - docs.rs - Rust encourages library maintainers to write documentation and functional code that can be turned into documentation for others. docs.rs hosts documentation for all crates on crates.io - newrustacean - A podcast on the Rust programming language - crates.io - "Just like rubygems" - Help: - There are a number of communities with active members that are always willing to help with your Rust questions --- class: middle, center, overlay # Thank you! ![](http://i.giphy.com/26uTsMNf5ic2JBSfu.gif) ???