DEV Community

loading...

Making Ruby Gem with Rust

kojix2 profile image kojix2 Updated on ・4 min read

Writing Ruby extensions in C is a popular way to speed up Ruby.
A few days ago on Reddit, I read a post titled Released a string metrics/distance gem written in Rust!

It seems that you can make Ruby Gems with Rust. I watched the contents of the repository and made a simple gem that calculates the Fibonacci number.

Create a Gem

Create a Gem template with bundler.

bundle gem fib -t rspec
cd fib

fib stands for Fibonacci.

Gemspec

Edit fib.gemspec. I like the simple one.

require_relative 'lib/fib/version'

Gem::Specification.new do |spec|
  spec.name          = "fib"
  spec.version       = Fib::VERSION
  spec.author        = ["kojix2"]
  spec.summary       = "Get the fibonacci number"
  spec.files         = Dir['lib/**/*', 'src/**/*.rs', 'Cargo.toml', 'LICENSE', 'README.md']
  spec.require_paths = ["lib"]
end

cargo init

Create Cargo.toml.

cargo init --lib

Cargo.toml

Add the following two lines to Cargo.toml.

[lib]
crate-type = ["cdylib"]

It should look like this.

[package]
name = "fib"
version = "0.1.0"
authors = ["author <soda_mail_siyo@mail.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]

cdylib is used when compiling a dynamic library to be loaded from another language(Ruby).

You now have a Gem template.

Add Rust code to calculate Fibonacci numbers

Open src/lib.rs and add the following functions.

#[no_mangle]
pub extern fn fib(n: u32) -> u32 {
    if n <= 1 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

Rakefile

Add the rust_buildtask to the Rakefile.

task :rust_build do
  `cargo rustc --release`
  `mv -f ./target/release/libfib.so ./lib/fib` # Change here according to your OS.
end

task :build => :rust_build
task :spec => :rust_build

Try rake rust_build to see if it works.

   Compiling fib v0.1.0 (/home/kojix2/Ruby/fib)
    Finished release [optimized] target(s) in 3.50s

Check the directories with the tree command.
You can see that libfib.so is located in the lib/fib directory.

image.png

Add rust_clean in the same way (This is optional).

task :rust_clean do
  `cargo clean`
  `rm -f ./lib/fib/libfib.so` # Change here according to your OS.
end

task :clean => :rust_clean

Try rake clean.

Then tree or exa -T

image.png

You will see that the target directory and libfib.so have been removed.

FFI

I use Foreign function interface (FFI) to call Rust from Ruby.
Ruby has two FFI libraries. fiddle and ruby-ffi. In a simple case like this one, either is fine.

Create the FFI module

Add the Rust functions as a Ruby method to this FFI module.
lib/fib/ffi.rb.

module Fib
  module FFI
  end
end

Overview

Fiddle

Fiddle is a standard Ruby library, so you can use it right away.

Create a new file lib/fib/ffi.rb.

require 'fiddle/import'

module Fib
  module FFI
    extend Fiddle::Importer
    dlload File.expand_path('libfib.so', __dir__)
    # Mac : libfib.dylib, Win : libfib.dll
    extern 'unsigned int fib(unsigned int n)' # like C language
  end
end

If Fiddle does not work on Windows, check this.

Ruby-FFI

Ruby-FFI is not a Ruby standard library. You need to edit fib.gemspec.

spec.add_dependency "ffi"

bundle update to install ruby-ffi.

Create a new file lib/fib/ffi.rb.

require 'ffi'

module Fib
  module FFI
    extend FFI::Library
    lib_name = "libfib.#{::FFI::Platform::LIBSUFFIX}"
    ffi_lib File.expand_path(lib_name, __dir__)
    attach_function :fib, [:uint], :uint
  end
end

Making Ruby methods

This time, I will call the Fibonacci number with the idiom.

Fib[3] # => 2

Open lib/fib.rb and edit it as follows.

require "fib/ffi" # 追加
require "fib/version"

module Fib
  def self.[](n)
    FFI.fib(n)
  end
end

Of course, you can add functions to the Fib module at first hand. However, sometimes you need to do something on the Ruby side before calling functions in other languages like Rust. Therefore, it is safer to create a special module for FFI and call its methods indirectly.

Overview

Now let's run the gem.

bundle exec bin/consle

Alt Text

It Works well.

Add a Test file

Open spec/fib_spec.rb.

RSpec.describe Fib do
  it "has a version number" do
    expect(Fib::VERSION).not_to be nil
  end

  it "calculates fibonatti" do
    expect(Fib[10]).to eq(55)
  end
end

Installation

rake install

Check if Gem works without Bundler.

Original article

Discussion

pic
Editor guide
Collapse
rhymes profile image
rhymes

Very straightforward, thanks!