RustでRubyのgemを書く part1

RubyGems 3.3.11でRust拡張の対応が実験的に入ったので、少し触ってみたメモ。

In particular, it includes experimental supppot for Rust extensions.

引用: https://blog.rubygems.org/2022/04/07/3.3.11-released.html

wasabi

2019年に鹿児島Ruby会議01で作ったRust製gemがあるので、これを修正しました。

speakerdeck.com

github.com

thermite の削除

refs: b16bfd1e2864adbb698f5225f86e7fd18da0b95e

thermite はRust拡張を作るために使用していたが、RubyGemsが対応したことで不要になったので削除する。

ext/wasabi/extconf.rb の内容は正直よく分かっていない。 サンプルコードであるrb-rust-gemのext/rust_ruby_example/extconf.rb をコピペしたら動いた。

GitHub Actionsでのビルド

refs: 4c8b35774b4b1d6e2b94c1b6dc7aee4bf0e97180

GitHub ActionsのUbuntuにはRust環境が含まれているので、以下のYAMLでビルド + テストが可能になる。

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@v3

    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true

    - run: gem update --system
    - run: bundle exec rake

実行結果を見ると、 .so のビルドが動いた後にRSpecが流れているのが確認できる。

▶︎ Run bundle exec rake
mkdir -p tmp/x86_64-linux/wasabi/2.7.6
cd tmp/x86_64-linux/wasabi/2.7.6
/opt/hostedtoolcache/Ruby/2.7.6/x64/bin/ruby -I. ../../../../ext/wasabi/extconf.rb
cd -
mkdir -p tmp/x86_64-linux/stage/lib/wasabi
install -c tmp/x86_64-linux/wasabi/2.7.6/wasabi.so lib/wasabi/wasabi.so
cp tmp/x86_64-linux/wasabi/2.7.6/wasabi.so tmp/x86_64-linux/stage/lib/wasabi/wasabi.so
/opt/hostedtoolcache/Ruby/2.7.6/x64/bin/ruby -I/home/runner/work/wasabi/wasabi/vendor/bundle/ruby/2.7.0/gems/rspec-core-3.9.0/lib:/home/runner/work/wasabi/wasabi/vendor/bundle/ruby/2.7.0/gems/rspec-support-3.9.0/lib /home/runner/work/wasabi/wasabi/vendor/bundle/ruby/2.7.0/gems/rspec-core-3.9.0/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb

Wasabi
  has a version number
  .sum
    1 + 2 = 3
  .call_to_s
    1.to_s
      is expected to eq "1"
    class with :to_s defined
      is expected to eq "foo"

Finished in 0.00192 seconds (files took 0.08639 seconds to load)
4 examples, 0 failures

rb-sys or ruby-sys

RustからRuby APIを呼ぶときに使用できそうなクレート。

  • oxidize-rb/rb-sys
    • RubyGemsの対応した人がコントリビュータ
    • 使い方がよく分からない...
    • 現在も更新されている
  • steveklabnik/ruby-sys
    • 2017年で更新停止
    • コード読めば、なんとなく使い方は分かる

仕組みがよく分かっていなくて、このコードでなぜRubyAPIが呼べるのかは謎。

Rust の実装

C拡張のgemでは Init_xxx が最初に呼ばれるので、ここでモジュールやメソッドを定義する。

#[no_mangle]
pub extern fn Init_wasabi() {
    unsafe {
        let rb_mod = class::rb_define_module(str_to_cstring("Wasabi").as_ptr());
        class::rb_define_singleton_method(rb_mod, str_to_cstring("sum").as_ptr(), rb_sum as CallbackPtr, 2);
        class::rb_define_singleton_method(rb_mod, str_to_cstring("call_to_s").as_ptr(), rb_call_to_s as CallbackPtr, 1);
    }
}

Wasabi.sum のRustの実装は以下の通りで、Value型を数字に戻して計算した後にValueに戻して返している。

// 引数の合計数を返す。
extern fn rb_sum(_mod: Value, a :Value, b: Value) -> Value {
    let a = unsafe { fixnum::rb_num2int(a) as i64 };
    let b = unsafe { fixnum::rb_num2int(b) as i64 };
    let sum = a + b;

    unsafe { fixnum::rb_int2inum(sum as SignedValue) }
}

Wasabi.call_to_s の実装はこんな感じ。

// 引数の `to_s` を呼んで返す。
extern fn rb_call_to_s(_mod: Value, obj: Value) -> Value {
    unsafe {
        let method_id = util::rb_intern(str_to_cstring("to_s").as_ptr());
        util::rb_funcallv(obj, method_id, 0, ptr::null())
    }
}

調査中

調べ終わったら、part2の記事を書きたい。

Rustの構造体をRubyで使う

構造体をData_Wrap_StructValueに変換する必要があるらしい。 ただ、この辺りのRubyのマクロをどうやってRustから呼び出すのか理解できてない...。

RustのメソッドをRubyで使う

関数をRubyで使う方法は分かったが、メソッドをRubyで使う方法はまだ分かっていない。 Rustの構造体 + メソッドをRubyのクラス + メソッドとして使いたい。