magnus というcrate を利用すると、超簡単に Ruby の拡張ライブラリが実装できます。
具体的には、Rust 側の記述はこんな感じになります。
use magnus::{define_class, function, method, prelude::*, Error}; #[magnus::wrap(class = "Point")] struct Point { x: isize, y: isize, } impl Point { fn new(x: isize, y: isize) -> Self { Self { x, y } } fn x(&self) -> isize { self.x } fn y(&self) -> isize { self.y } fn distance(&self, other: &Point) -> f64 { (((other.x - self.x).pow(2) + (other.y - self.y).pow(2)) as f64).sqrt() } } #[magnus::init] fn init() -> Result<(), Error> { let class = define_class("Point", Default::default())?; class.define_singleton_method("new", function!(Point::new, 2))?; class.define_method("x", method!(Point::x, 0))?; class.define_method("y", method!(Point::y, 0))?; class.define_method("distance", method!(Point::distance, 1))?; Ok(()) }
これをビルドすると、Ruby 側に Point クラスが生えます。 Ruby の世界 と Rust の世界 の間で相互に自動で型が変換されていてすごいですね。
point_1 = Point.new(0, 0) point_2 = Point.new(100, 100) point_1.distance(point_2) # => 141.4213562373095
この記事では、渡された文字列をシャッフルする Shuffle という gem の実装を通じて、Rust で拡張ライブラリを作る方法を説明します。
なお、実行環境は macOS Monterey 12.4、CPU は M1 Pro です。とはいえ、他の環境でも似たような手順で行けると思います。
gem を作る
拡張ライブラリを入れるための gem を作っていきます。
gem を初期化する
$ bundle gem shuffle
$ cd shuffle
諸々のバージョンを揃える
Ruby のバージョンを 3.1.1 以上にする必要があります*1。 2022/06/12 現在では、 3.1.2 を入れておけば良いと思います。
$ rbenv install 3.1.2 $ rbenv local 3.1.2
また、Rubyems のバージョンを 3.3.11 以上にする必要があります*2。
$ gem -v 3.3.15 # 古い場合はバージョンを上げる $ gem update --system
Rust をセットアップする
rustup を利用して適当に Rust をセットアップします。 1.57.0 以上をインストールしてください*3。
gemspec を適当に埋める
shuffle.gemspec
の TODO になっている欄を適当に埋めます。埋めないとこの先の作業ができません。
参考: fix gemspec · genya0407/shuffle@5563f9c · GitHub
Rust で拡張ライブラリを作る
いよいよ拡張ライブラリを作っていきます。
拡張ライブラリをビルドする環境を作る
まず、拡張ライブラリをビルドするのに必要な gem を dependency として追加します。
shuffle.gemspec
に以下を書き加え、bundle install を実行します。
spec.add_dependency "rake-compiler" spec.add_dependency "rb_sys"
$ bundle install
次に、bundle exec rake compile
で拡張ライブラリをビルドするため、 Rakefile
に以下を書き加えます。
require "rake/extensiontask" Rake::ExtensionTask.new("shuffle") do |ext| ext.lib_dir = "lib/shuffle" ext.source_pattern = "*.{rs,toml}" end
そして、 ext/shuffle/extconf.rb
を作成します。
require "mkmf" require "rb_sys/mkmf" create_rust_makefile("shuffle/shuffle")
最後に、 shuffle.gemspec
に extconf.rb
を認識させるため、以下を書き加えます。
spec.extensions = ["ext/shuffle/extconf.rb"]
ここまでで、Rust 拡張をビルドする準備が整いました。
参考: setup rake-compiler && rb_sys · genya0407/shuffle@61af062 · GitHub
Rust を書く
まず、 ext/shuffle
で Rust のプロジェクトを初期化します。
$ cd ext/shuffle $ cargo init --lib $ echo 'target' >> .gitignore
そして、 Cargo.toml
に以下を書き加えます。
[lib] crate-type = ["cdylib"] # 拡張ライブラリを作るために必要 [dependencies] magnus = "0.3" rand = "0.8.5" # 文字列をシャッフルするのに使う
最後に、文字列をシャッフルするクラスを定義しましょう。
ext/shuffle/src/lib.rs
に以下を記述します。
use magnus::{define_class, function, method, prelude::*, Error}; use rand::seq::SliceRandom; #[magnus::wrap(class = "Shuffle")] struct Shuffle { original: String, } impl Shuffle { fn new(original: String) -> Self { Self { original } } fn shuffle(&self) -> String { let mut rng = rand::thread_rng(); let mut v = self.original.chars().collect::<Vec<_>>(); v.shuffle(&mut rng); v.into_iter().collect() } } #[magnus::init] fn init() -> Result<(), Error> { let class = define_class("Shuffle", Default::default())?; class.define_singleton_method("new", function!(Shuffle::new, 1))?; class.define_method("shuffle", method!(Shuffle::shuffle, 0))?; Ok(()) }
そしておもむろに bundle exec rake compile
を実行すると、 lib/shuffle/shuffle.bundle
というファイルが生成されます。
$ bundle exec rake compile
$ file lib/shuffle/shuffle.bundle
lib/shuffle/shuffle.bundle: Mach-O 64-bit dynamically linked shared library arm64
このファイルを require
すると、 Shuffle
クラスが使えます。
$ bundle exec irb irb(main):001:0> require './lib/shuffle/shuffle' => true irb(main):002:0> Shuffle.new('abc').shuffle => "bca"
gem のお作法的には、 lib/shuffle.rb
から require_relative しておくのが良いです。
# frozen_string_literal: true require_relative "shuffle/version" require_relative "shuffle/shuffle" # これを追加 class Shuffle class Error < StandardError; end # Your code goes here... end
なお、 bundle gem
実行時に、Shuffle
は module として宣言されているので、そのままだと「Shuffle は class じゃなくて module だよ」みたいなエラーが出ます。
これを回避するために、 lib/shuffle.rb
や lib/shuffle/version.rb
を修正しておきましょう。
参考:implement extension · genya0407/shuffle@4389ef1 · GitHub
作った gem を利用する
RubyGems に publish するのも何なので、 github からインストールしてみます。
先程の shuffle gem を github に push しておきつつ、以下のような Gemfile
を作って bundle install
してみましょう。
$ cat Gemfile # frozen_string_literal: true source "https://rubygems.org" gem 'shuffle', github: 'genya0407/shuffle', branch: 'trunk' $ bundle install Fetching https://github.com/genya0407/shuffle.git Fetching gem metadata from https://rubygems.org/. Fetching rake 13.0.6 Installing rake 13.0.6 Using bundler 2.3.15 Fetching rake-compiler 1.2.0 Fetching rb_sys 0.9.4 Installing rb_sys 0.9.4 Installing rake-compiler 1.2.0 Using shuffle 0.1.0 from https://github.com/genya0407/shuffle.git (at trunk@f24fe1f) Bundle complete! 1 Gemfile dependency, 5 gems now installed. Bundled gems are installed into `./vendor` $ bundle exec irb irb(main):001:0> require 'shuffle' => true irb(main):002:0> Shuffle.new('abcdefg').shuffle => "gdecafb"
このように、 gem として利用できることが確認できました。 なお、利用者側の環境にも Rust が必要である点に注意して下さい。
magnus に関する雑感
magnus の型変換がちゃんとしててすごいと思いました。 感動ポイントを箇条書きします*4。
- 関数の型宣言を見て、勝手に型変換をやってくれる
- GitHub - matsadler/magnus: Ruby bindings for Rust の表のとおり、変換の対応付けが妥当。
- しかも、変換不能な場合は IllegalArgument エラーを raise してくれる。panic したりしない。
- 型変換ができないときは手動でも変換できる
- Ruby のオブジェクトと Rust の構造体の紐付けを勝手にやってくれる
- C拡張みたいに TypedData_Wrap_Struct とかしなくていい。
全体的に、「そうなってほしい」という思いが達成されていて、おまじないが少ない作りになっているのが良いです。
まとめ
拡張ライブラリを実装したくなる機会は現状ではそれほどないと思いますが、このレベルまで簡単に実装できるようになったことで、Rust の拡張ライブラリを作るという選択肢が一般的になってくると Ruby の将来という観点でも面白い と思いました。
例えば、 GitHub - genya0407/reing_print_image は、 GitHub - genya0407/reing_text2image を拡張 gem にしたものです。
reing_text2image はもともと、 Rails で作った 自作質問箱 でOGPの画像を生成するために作ったツールでした。
Crystal 別にそんなに好きじゃないし業務で使ってないけど、Ruby ぽくて速いので競プロするときに使おうかなと思っている https://t.co/rb9kAJMXeM #reing
— 𝘼𝙧𝙧𝙖𝙮-𝙨𝙖𝙣 (@genya0407) May 29, 2022
以前はこれをCLI ツールとしてRuby から呼び出して、一度ディスクに画像ファイルとして書き出したものを Ruby から read して send_data で返却するという、結構すごい作りをしていました。
しかし、Rust を使った拡張 gem が手軽に作れるようになったことで、ファイルを介すことなく、png のバイナリを直接 Rust → Ruby で受け渡せるようになりました。
reing_print_image/reing_print_image_spec.rb at trunk · genya0407/reing_print_image · GitHub
このように、全体的には Ruby や Rails で実装しておき、無理なところだけ Rust に切り出す、ということができるようになったことで、Ruby が関与できる世界が広がっていく*6のではないかと思いました。
Rust で拡張ライブラリを作るのは思いの外面白かったので、他にもネタを見つけてやっていきたいと思います。
*2:Rust extension support を利用するため 3.3.11 Released - RubyGems Blog
*3:magnus がサポートしているのが 1.57.0 以上なので GitHub - matsadler/magnus: Ruby bindings for Rust
*4:筆者は拡張ライブラリを真面目に書いたことがないので、的はずれなことを言っている可能性も高いですが...
*5:Rust の String は UTF-8 として valid な文字列しか入らない
*6:あるいは縮小を緩和できる