連記事目次
- Ruby/Rust 連携 (1) 目的
- Ruby/Rust 連携 (2) 手段
- Ruby/Rust 連携 (3) FFI で数値計算
- Ruby/Rust 連携 (4) Rutie で数値計算①
- Ruby/Rust 連携 (5) Rutie で数値計算② ベジエ
- Ruby/Rust 連携 (6) 形態素の抽出
- Ruby/Rust 連携 (7) インストール時ビルドの Rust 拡張 gem を作る
- Ruby/Rust 連携 (8) Rust 拡張 gem を magnus で作る
はじめに
この連記事の 前の回((7))を書いたのはちょうど五年前だ。
あれから Ruby と Rust を繋ぐ技術も変わった。いまの主流は magnus であるらしい1。
(7) とほぼ同じ主題を,magnus を使ってやってみよう。
つまり,Rust で書かれたコードを含む gem を作ってみるのだ。
五年前に比べてずっと簡単になっている。
なお,筆者は Rust に関して完全素人。ツッコミ歓迎。
題材
Ruby の組込みメソッドや標準添付ライブラリーには,一様分布以外の分布を持つ乱数を生成する機能が無い。
そこで,以下のように使える my_rand という gem を,Rust を使って作ってみる。
require "my_rand"
# 正規分布の乱数生成器
# 平均 1.2,標準偏差 0.7
normal = MyRand::Normal.new(1.2, 0.7)
normal.sample # `sample` が呼ばれるたびに乱数を返す
# ポアソン分布の乱数生成器
# 平均(=分散)2.0
poisson = MyRand::Poisson.new(2.0)
poisson.sample # `sample` が呼ばれるたびに乱数を返す
乱数生成そのものは Rust のライブラリーを使う。
準備
Ruby と Rust はインストールされているとする。
Rust のインストールはとても簡単なので略す。
Bundler は最新版にしておこう。
今回使用したものの各種バージョンは以下の通り(本記事執筆時点の最新版)。
- ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin24]
- Bundler version 2.7.2
- rustc 1.90.0 (1159e78c4 2025-09-14) (Homebrew)
作り方
gem の雛形を生成
gem を作るのは Bundler のコマンドで bundle gem
とするのが楽。
最低限必要なファイルを全て含んだ雛形を生成してくれるし,最新版の Bundler を使えば,その時点で「中の人が最も望ましいと考えているであろう」スタイルで書かれたファイル群ができる(知らんけど)。
オプションとして --ext rust
を与えると,Rust を用いた拡張ライブラリーを作る前提のファイルが生成される。
具体的にはこうだ:
bundle gem --ext rust my_rand
引数に与えた my_rand
は,この gem の基本のモジュール名(MyRand)となる。
Rust で使用するライブラリーを指定する
生成された ext/rand_norm/Cargo.toml には,使用する(=依存する)ライブラリー・クレートを書いた以下のような箇所がある。(Cargo.toml というファイルは 2 箇所にあるので混同せぬよう)
[dependencies]
magnus = { version = "0.6.2" }
magnus を使うということが,最初から書かれている。
バージョンの指定 0.6.2
は「0.6.2 以上,0.6.3 未満」という意味だ2。
なお,バージョンを指定したいだけなら,この行は
magnus = "0.6.2"
と書いてもよい3。
さて,今回は rand と rand_distr というクレートを使いたいので,magnus の行に続けて
rand = "0.9.2"
rand_distr = "0.5.1"
を追記する。
これらのバージョンは,この記事執筆時点での最新版とした。
Rust コードを書く
Rust コード本体は ext/my_rand/src/lib.rs に書く。
use magnus::{function, method, prelude::*, Error, Ruby};
use rand_distr::Distribution;
// 正規分布
#[magnus::wrap(class = "MyRand::Normal")]
struct Normal {
generator: rand_distr::Normal<f64>,
}
impl Normal {
fn new(mean: f64, std_dev: f64) -> Self {
let generator = rand_distr::Normal::new(mean, std_dev).unwrap();
Self { generator }
}
fn sample(&self) -> f64 {
self.generator.sample(&mut rand::rng())
}
}
// ポアソン分布
#[magnus::wrap(class = "MyRand::Poisson")]
struct Poisson {
generator: rand_distr::Poisson<f64>,
}
impl Poisson {
fn new(lambda: f64) -> Self {
let generator = rand_distr::Poisson::new(lambda).unwrap();
Self { generator }
}
fn sample(&self) -> f64 {
self.generator.sample(&mut rand::rng())
}
}
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = ruby.define_module("MyRand")?;
// 正規分布
let class = module.define_class("Normal", ruby.class_object())?;
class.define_singleton_method("new", function!(Normal::new, 2))?;
class.define_method("sample", method!(Normal::sample, 0))?;
// ポアソン分布
let class = module.define_class("Poisson", ruby.class_object())?;
class.define_singleton_method("new", function!(Poisson::new, 1))?;
class.define_method("sample", method!(Poisson::sample, 0))?;
Ok(())
}
Rust コードとして記述するのはこれで全てだ。
sample
メソッドが Normal と Poisson の両方に同じ定義で書いてあるのは無駄な感じがする。
1 箇所にまとめる方法が無いものかと思うが,私の乏しい Rust 理解では分からなかった。
gemspec の編集
my_rand.gemspec というファイルに,この gem の素性をひととおり記述する。
公開する gem であればきちんと書かなければならないが,今回はローカルで使用する単なる試作なので,マジメにはやらない。
bundle gem
が作った雛形の gemspec ファイルには
spec.summary = "TODO: Write a short summary, because RubyGems requires one."
のように,「TODO:」の入った箇所がいくつかあるので,本来であればそこを適切に記述する。
「TODO:」が入っているとエラーが出てビルドに失敗するので,とりあえずこの「TODO:」を全部削除しよう。
それから,
spec.homepage = "Put your gem's website or public repo URL here."
とか
spec.metadata["homepage_uri"] = spec.homepage
のように,URL を指定する箇所もいくつかある。
雛形のままだと「URL の形式になってねえじゃん」と,これまたビルドに失敗する。
今回は,これらをすべてコメントアウトしておこう。
gem のビルド
gem のビルドとは,gem を作ること。拡張子が .gem
のファイルが一つできる。
このファイルを配布してインストールすればその gem を使うことができる。
ビルドは
rake build
とするだけ。
これで Rust コードのコンパイルを含む一連の処理が走り,pkg/my_rand-0.1.0.gem というファイルが出来る。
これが gem だ。
gem のインストール
gem を開発しているディレクトリー上なら
rake install
とすれば出来た gem がグローバルにインストールされる。
このとき,build
タスクも自動的に実行してくれるので,いちいち build
→install
などとしなくよい。
また,出来た my_rand-0.1.0.gem をどこかに持って行って
gem install my_rand-0.1.0.gem
などとすればそこにインストールすることもできる。
使ってみよう
出来た gem を使うサンプルコードは記事の冒頭に掲げておいたが,乱数が 1 個出るだけではつまらない。
ホンマにそういう分布の乱数なんか?ということが実感できるサンプルを。
require "my_rand"
def show_distribution(generator, iteration)
bins = Array.new(iteration){ generator.sample.round }.tally
bins.default = 0
range = Range.new(*bins.keys.minmax)
max = bins.values.max
range.each do |x|
puts "%4d %s" % [x, "*" * (bins[x].fdiv(max) * 40).round]
end
end
mean = -1.5
std_dev = 2.3
puts "正規分布(平均 #{ mean },標準偏差 #{ std_dev })"
normal = MyRand::Normal.new(mean, std_dev)
show_distribution(normal, 10000)
puts
lambda = 1.8
puts "ポアソン分布(平均 #{ lambda })"
poisson = MyRand::Poisson.new(lambda)
show_distribution(poisson, 10000)
実行結果:
正規分布(平均 -1.5,標準偏差 2.3)
-10
-9
-8 *
-7 **
-6 ******
-5 **************
-4 **********************
-3 *********************************
-2 **************************************
-1 ****************************************
0 **********************************
1 ***********************
2 *************
3 ******
4 ***
5 *
6
7
ポアソン分布(平均 1.8)
0 **********************
1 ****************************************
2 ***********************************
3 **********************
4 *********
5 ***
6 *
7
8
うん,それっぽいっすね!
補足
ポアソン分布乱数の返り値の型
ポアソン分布の確率変数は 0 以上の整数だ。
にもかかわらず,Rust の rand_distr::Poisson
の sample
の返り値は浮動小数点数になっている。
これはどうも,整数型だと(その型で表せる)最大値を超える値が取れないから,らしい。
参考:Integer vs FP return type(rand_distr クレートのドキュメントの Poisson の一部)
Ruby だと Integer に最大値は無いので,こういうところに気付いにくいね。
Rust のコンパイルはいつ走るのか
gem をビルドするときにコンパイラーが走ったので,
「えっ,これコンパイル済みファイルが gem に含まれるやり方なのか?」
と思ったけど,そうではなかった。
生成された gem は 7 KB しかなかった。
gem をインストールするときに改めてコンパイラーが走った。
magnus の理解
bundle gem
コマンドで magnus を使う雛形が出来て,それに最低限の書き換えを施しただけで gem が作れてしまったので,はっきり言って私は magnus の仕様をほとんど理解していない。
たとえば,Rust コードの
#[magnus::wrap(class = "MyRand::Poisson")]
なんかは,「まあそう書くのね」くらいの認識であり,この記述の意味が分かってはいない。
Ruby のクラスにシングルトンメソッドやインスタンスメソッドを生やすところで
function!(Normal::new, 2)
とか
method!(Normal::sample, 0)
といったマクロが使われているが,この 2
とか 0
というのは作ろうとするメソッドの引数の個数だ。
なんでシングルトンメソッドに function!
を使って,インスタンスメソッドに method!
を使うのか,分からん。
感想
こんなに簡単でインカ帝国?
-
magnus は Rust のライブラリー・クレートの名前。Ruby と Rust を繋ぐ主流の技術としては,この他に rb-sys というクレートもあるが,両者はレイヤーが違っており,ライバルではない。rb-sys は最も低いレイヤーを担当するもので,magnus もこれを利用している。 ↩
-
Ruby の Gemfile の書き方でいうと
~>
を使った書き方に似ているが,メジャーバージョンが 1 以上の場合は違う。詳しくは Cargo 公式リファレンスの specifying-dependencies.html#version-requirement-syntax を参照。 ↩ -
{ }
を使うと,リポジトリーやそのタグなど,バージョン以外のことが書ける。 ↩