3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby/Rust 連携 (8) Rust 拡張 gem を magnus で作る

Last updated at Posted at 2025-10-19

連記事目次

はじめに

この連記事の 前の回((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 箇所にあるので混同せぬよう)

ext/rand_norm/Cargo.toml(抜粋)
[dependencies]
magnus = { version = "0.6.2" }

magnus を使うということが,最初から書かれている。
バージョンの指定 0.6.2 は「0.6.2 以上,0.6.3 未満」という意味だ2

なお,バージョンを指定したいだけなら,この行は

magnus = "0.6.2"

と書いてもよい3

さて,今回は randrand_distr というクレートを使いたいので,magnus の行に続けて

ext/rand_norm/Cargo.toml(抜粋)
rand = "0.9.2"
rand_distr = "0.5.1"

を追記する。
これらのバージョンは,この記事執筆時点での最新版とした。

Rust コードを書く

Rust コード本体は ext/my_rand/src/lib.rs に書く。

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 ファイルには

my_rand.gemspec(抜粋)
  spec.summary = "TODO: Write a short summary, because RubyGems requires one."

のように,「TODO:」の入った箇所がいくつかあるので,本来であればそこを適切に記述する。

「TODO:」が入っているとエラーが出てビルドに失敗するので,とりあえずこの「TODO:」を全部削除しよう。

それから,

my_rand.gemspec(抜粋)
  spec.homepage = "Put your gem's website or public repo URL here."

とか

my_rand.gemspec(抜粋)
  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 タスクも自動的に実行してくれるので,いちいち buildinstall などとしなくよい。

また,出来た 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::Poissonsample の返り値は浮動小数点数になっている。

これはどうも,整数型だと(その型で表せる)最大値を超える値が取れないから,らしい。
参考: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! を使うのか,分からん。

感想

こんなに簡単でインカ帝国?

  1. magnus は Rust のライブラリー・クレートの名前。Ruby と Rust を繋ぐ主流の技術としては,この他に rb-sys というクレートもあるが,両者はレイヤーが違っており,ライバルではない。rb-sys は最も低いレイヤーを担当するもので,magnus もこれを利用している。

  2. Ruby の Gemfile の書き方でいうと~> を使った書き方に似ているが,メジャーバージョンが 1 以上の場合は違う。詳しくは Cargo 公式リファレンスの specifying-dependencies.html#version-requirement-syntax を参照。

  3. { } を使うと,リポジトリーやそのタグなど,バージョン以外のことが書ける。

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?