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_build
task 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.
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
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
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.
Now let's run the gem.
bundle exec bin/consle
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.
Top comments (1)
Very straightforward, thanks!