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があるので、これを修正しました。
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年で更新停止
- コード読めば、なんとなく使い方は分かる
仕組みがよく分かっていなくて、このコードでなぜRubyのAPIが呼べるのかは謎。
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_StructでValueに変換する必要があるらしい。 ただ、この辺りのRubyのマクロをどうやってRustから呼び出すのか理解できてない...。
RustのメソッドをRubyで使う
関数をRubyで使う方法は分かったが、メソッドをRubyで使う方法はまだ分かっていない。 Rustの構造体 + メソッドをRubyのクラス + メソッドとして使いたい。