32
12

More than 3 years have passed since last update.

この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 1日目の記事です。

プログラムのパフォーマンス向上のためにRubyからCで書かれた処理を呼び出したいという場合があるとき、拡張ライブラリを書いたりFFIを使って処理を呼び出したりします。
今回はRustで書かれた処理をRubyで呼び出す方法を調べてみました。

拡張ライブラリを書く方法

RubyにはCのためのAPIがあり、これを使うことで拡張ライブラリを作ることができます。RustにはCのプログラムとリンクさせるライブラリを作ることができるので、その機能を利用してRustでRubyの拡張ライブラリを作ることができます。

詳細は以下の記事をご参照ください。
RustだけでRuby native extensionを書く

FFIで呼び出す方法

Cで拡張ライブラリを記述するよりも、より簡単な方法としてFFI(Foreign function interface)を利用する方法もあります。
FFIを使った場合にはC拡張を書くことなく、利用することができるので、より簡単に使うことができます(内部的にはC拡張の仕組みと同じものが動きます)。

詳細は以下の記事をご参照ください。
RubyからRustの関数をつかう → はやい

Helixを使う方法

Rustで簡単に拡張ライブラリを書くための方法としてhelixというgemが
あります。Qiita界隈ですとHelixといえばネームスペースはキーボードに占有されている気もしますが、RustとRubyのブリッジということで実在するが名前の由来のようです。
Getting Startedのページを見るといきなりRuby on Railsのプロジェクトから作り始めるなかなかに攻めた内容になっていますが、今回はシンプルにgemを作って、それを呼び出す方法を見てみます。
前章の参照記事で書かれているものとほぼ同じフィボナッチ数を求める関数を作成していきます。

gemテンプレートの作成

まずはgemのプロジェクトの雛型を作ります。

$ bundle gem helix_fib
Creating gem 'helix_fib'...
MIT License enabled in config
Code of conduct enabled in config
      create  helix_fib/Gemfile
...
$ cd helix_fib

プロジェクトの作成が完了したら、helix_fib.gemspecに
以下の内容を追記します。

spec.add_runtime_dependency "helix_runtime", "= 0.7.5"

またspec.authorsなどTODOが記載されている項目は適当に記入します。
記入が終わったらbundle installを行って依存関係にあるhelix_runtimeをインストールします。

bundle install --path=vendor/bundle

次にRakeファイルを以下のように書き換えてbuild時にhelix_runtimeが使われるように修正します。

require 'bundler/setup'
require 'helix_runtime/build_task'

HelixRuntime::BuildTask.new do |t|
end

task :default => :build

rustテンプレートの作成

まず以下のコマンドを実行してrustのプロジェクトを作成します。

$ cargo init --lib

このコマンドによってCargo.tomlとsrc/lib.rsが作られます。
他言語用のダイナミックなシステムライブラリを作成したいためにCargo.tomlに以下の記載を加えます。

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

またdependenciesにはhelixを加えます

[dependencies]
helix = "0.7.5"

次にRustのコードを書いていきます。src/lib.rsに以下の内容を記述します。


#[macro_use]
extern crate helix;

ruby! {
    class HelixFIB {
        struct {
        }

        def initialize(helix) {
            HelixFIB{helix}
        }

        def fib(&self, n: u32) -> u32 {
            if n <= 1 {
                n
            } else {
                self.fib(n - 1) + self.fib(n - 2)
            }
        }
    }    
}

lib/helix_fib.rbの先頭に以下の2行を加えてビルドが通るように修正します。

require 'helix_runtime'
require 'helix_fib/native'

ビルドと動作確認

rakeコマンドを行ってrustのコードをビルドします。

$ bundle exec rake
{}
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
...
    Finished release [optimized] target(s) in 0.08s

ビルドが完了したらbin/consoleで動作を確認します。

$ bin/console
irb(main):001:0> helix_fib = HelixFIB.new
=> #<HelixFIB:0x00007fffc4a4afd0>
irb(main):002:0> helix_fib.fib(40)
=> 102334155

rustで書いたコードがrubyのクラスとして呼び出されて、関数が機能していることが確認できます。

gem化とrubyプロジェクトからの呼び出し

gemのパッケージ化をした際にinstall時にビルドされる必要があるためhelix_fib.gemspecに以下の内容を追記します。

spec.extensions = %w[extconf.rb] 

またextconf.rbには以下の内容を記述します。

execonf.rb
abort "Rust compiler required (https://www.rust-lang.org/)" if `which rustc`.empty?

File.open("Makefile", "wb") do |f|

  f.puts(<<EOD)
all:
\tbundle --deployment --path vendor/bundle
\tbundle exec rake
clean:
install:
\trm -r vendor/bundle target
EOD

end

gemのbuildの際にディレクトリの内容をgit ls-filesで検索して依存性を解決するので、これまでの作業内容をgitに記録します。

git add .

gem buildを行うとgemファイルが完成します。

$ gem build helix_fib
  Successfully built RubyGem
  Name: helix_fib
  Version: 0.1.0
  File: helix_fib-0.1.0.gem

新しいディレクトリを作成して、bundle initして作成されたGemfileにhelix_runtimeとhelix_fibを追記します。

gem "helix_runtime"
gem "helix_fib"

vender/cacheのディレクトリを作成したあとに先ほど作成したhelix_fib-0.1.0.gemをコピーして、bundle install --path=vendor/bundleを行います。
helix_fibを呼び出す処理は以下のように記述します。

sample.rb
require 'helix_fib'

helix_fib = HelixFIB.new
puts helix_fib.fib(40)

作成したファイルを実行すると以下のような結果が返ってきてRustで作成したファイルが実行されていることがわかります。

$ bundle exec ruby sample.rb
102334155

Wasmを使う方法

Rustで書かれたコードをWasmにコンパイルする手法はよく知られているものだと思いますが、最近ではCloudfrare Workersや、Compute@Edgeなど、フロントエンドだけでなく、サーバーサイドよりの領域でも使われるようになっていました。そしてWASIというWASMをウェブブラウザー以外から呼び出すための仕様も策定されています。
今回はRustで書かれたコードをWasmにコンパイルして、それをRubyから呼び出す方法について記載しようと思います。
wasmerというスタンドアローンなWasmのランタイムとそれを呼び出すgemが存在するので、今回はこれを利用します。

環境の準備

まずはrustupを使ってターゲットとするコンパイラにwasmを追加します。

rustup target add wasm32-unknown-unknown

プロジェクトの作成とCargo.tomlの整備

cargoコマンドを使ってWasm用のプロジェクトを作成します。

$ cargo new --lib wasm_fib

Cargo.tomlに以下の内容を追記します。

Cargo.toml
...
[lib]
crate-type =["cdylib"]

プログラムの作成

src/lib.rsに以下の内容を記載します。多言語から呼び出すので、[no_mangle]を付けて関数のマングリングをなくします。

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

Wasmへのコンパイル

cargo buildでwasmへのコンパイルを行います。

$ cargo build --target wasm32-unknown-unknown

コンパイルが完了するとtarget/wasm32-unknown-unknown/debugにwasm_fib.wasmが作成されます。

Rubyのプロジェクトの作成

Wasmを呼び出すためのRubyのプロジェクトを作成します。
新しいディレクトリを作成してbundle initを行って作成されたGemfileに以下の記述を追加して、bundle installを行います。

gem "wasmer"

Wasmを呼び出すプログラムの作成

前章で作成したwasm_fib.wasmをプロジェクトのディレクトリにコピーした後にsample.rbというファイルを作成し、以下の記述を行います。

sample.rb
require "wasmer"

bytes = IO.read "wasm_fib.wasm", mode: "rb"
instance = Wasmer::Instance.new bytes
puts instance.exports.fib 40

wasmのバイト列をWasmer::Instance.newの引数として渡すことで、wasmがインスタンス化され、関数を呼べるようになります。
プログラムを実行して結果を確認します。

$ bundle exec ruby sample.rb
102334155

ベンチマーク

rubyのベンチマーク関数を使ってフィボナッチ数の関数に40を渡したときの結果を出力してみます。

ruby

$ ruby fib.rb
                 user     system      total        real
fibonatti   11.750000   0.000000  11.750000 ( 11.748531)
102334155

ffi

$ ruby rust-fib.rb
                 user     system      total        real
fibonatti    0.593750   0.000000   0.593750 (  0.588983)
102334155

拡張ライブラリ

$ ruby rust-fib.rb
                 user     system      total        real
fibonatti    1.218750   0.000000   1.218750 (  1.232219)
102334155

Helix

 bundle exec ruby sample.rb
                 user     system      total        real
fibonatti    0.625000   0.000000   0.625000 (  0.627916)
102334155

Wasm

$ bundle exec ruby sample.rb
                 user     system      total        real
fibonatti    5.125000   0.000000   5.125000 (  5.144103)
102334155

結果としては、Ruby < Wasm < 拡張ライブラリ < Helix < FFI
の順番になりました。拡張ライブラリがHelixより遅いのはもっと検証が必要かもしれませんが、Wasmはランライムの呼び出しにコストがかかっているように感じました。
しかし、いずれの方法でもRubyに比べてパフォーマンスは向上しているようです。

まとめ

Helixをつかうことによって、RubyのC APIの内容をあまり知らずとも、Rustで拡張ライブラリを書くことができるようになりました。
ruby!やclassというおよそRustとは思えないコードであったり、
多言語から呼び出す処理に#[no_mangle]がないなど、不思議な感じが
する部分もありますが、簡単に拡張ライブラリが書けるということには少なからずメリットがあると思います。
Wasmに関してはFFIと同じような感覚があります。Rustで記述されたものをRubyで呼び出すためにWasmにする必要はなさそうに思えますが、wasmerの他にもwasmtimelucetなど、いくつかランタイムが登場していているので、興味深い分野だと思いました。

お知らせ

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページWebエンジニア詳細ページよりお問い合わせ下さい。
あるいは、Qiita Jobs引越し侍Webエンジニアチーム自社サービスWebエンジニア/インフラからサービス改善まで!求む!引越し侍大規模リプレイスにおけるフロントエンドのリードエンジニア募集!の記事をみて、興味が出てきた方は是非Qiita Jobsのチャットでメッセージをください。

明日

明日はfujimkの記事です。
きっとこれがQiita初投稿かな。皆さんお楽しみに。

32
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
12