Ateam Group U-30 のカレンダーの2日目はQiitaの @kyntk が担当します。
今年、RubyGems 3.3.11でRust extensionsのexperimentalリリースがありました。
変更されたPull Requestは↓です。
この変更でCargoBuilder
が実装され、Cargo.toml
ファイルを見つけるとcargoでRustのビルドができるようになりました。
今まではRust用にビルドの設定をする必要があったのですが、かんたんにビルドができるようになりました。
ちょうど、Qiita社でもRustを使う機会がありそうだったので、勉強がてらRustでgemを作成してみました。
Rust拡張は2022年11月時点でexperimentalです。
仕様が変わる可能性があるのでご注意ください。
$ gem -v
3.3.25 # larger than 3.3.11
また、現時点ではマージされていないですが、scaffoldのコマンドが追加される可能性があります。
下記のPull Requestで議論されていますが、今回私が作ったものとディレクトリ構造が変わりそうです。
こちらが正式にマージされた場合はそちらに則って作成してください。
なぜRustなのか
Rustは実行速度や詳細なメモリ管理などの特徴を持ち高いパフォーマンスを出せるだけでなく、メモリ安全性やデータ競合を防ぐ仕組みを持っています。
またRustはC互換のAPIも持っており、Cとの統合もしやすかったようです。
RubyGemsにはすでにC拡張があり、Cで書かれたコードをGemとしてビルドすることができますが、冒頭のリリースによってRustでもGemを実装することができるようになりました。
作ってみた
サンプルコードはこちらにあります。
実際にはDockerfileで設定していましたが、この記事では順に説明していきます。
まずはgemを3.3.11以上にアップデートします。
$ gem update --system
$ gem -v
3.3.25
そして今回はgreeting
というgemを作っていきます。
rspecやrubocop、ライセンスなどはお好みで設定してOKです。
$ bundle gem greeting
$ cd greeting
初期のgemspecにはTODO
がたくさんあるので、そこを書き換えていきます。
$ bundle install
$ bundle exec rake
gemspecにTODOが残っていると失敗することがありますが、rspecなどのrakeタスクが実行できることを確認します。
Rustの環境構築
rustupというツールを使ってRustの環境を構築します。
cargoはRustのパッケージマネージャーです。
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ cargo init --lib
$ cargo test
cargo testでテストが動くことを確認します。
rustでgemの実装
cargo init
をすると、src/lib.rs
などのファイルが作成されます。
これからはこのlib.rsファイルをいじっていきます。
Rustは勉強中なので、このコード自体はあまり当てにしないでください。
fn say() -> String {
format!("Hello, World!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = say();
assert_eq!(result, "Hello, World!");
}
}
magnus crateのインストール
次はRubyからRustのコードを実行できるようにするのですが、今回はmagnusというライブラリを使うことにします。
magnusはRubyとRustの間の型の変換も行ってくれます。
[package]
name = "greeting"
version = "0.1.0"
edition = "2021"
[dependencies]
+ magnus = { git = "https://github.com/matsadler/magnus", branch = "main" }
+ [lib]
+ crate-type = ["cdylib"]
crate-type = ["cdylib"]
について
動的システムライブラリとしてビルドするための設定らしいです。
Ruby拡張のときはこの設定が必要とのことです。
Rubyのメソッドの追加
+ use magnus::{define_module, function, Error};
+
fn say() -> String {
format!("Hello, World!")
}
+ #[magnus::init]
+ fn init() -> Result<(), Error> {
+ let module = define_module("Greeting")?;
+ module.define_module_function("say", function!(say, 0))?;
+ Ok(())
+ }
init関数を定義して、Greeting moduleにsay
メソッドを定義します。
Buildのための設定
gemspecに以下の2行の設定を追加します。
+ spec.extensions = ["Cargo.toml"]
+ spec.files = ["Cargo.toml", "Cargo.lock", "src/lib.rs"]
spec.extensions
はgemのビルドのときに、rust extensionのパスを指定するために使います。
C拡張ではextconf.rb
などを指定するプロパティです。
gemのビルド
gem buildコマンドでビルドをします。
先程Cargo.tomlのパスを指定したので、RubyGemsによって自動でCargoBuilderが選択されてcargo buildが実行されます。
ビルドの設定をしていないのにビルドができちゃいました!
$ docker compose run app gem build greeting.gemspec
Successfully built RubyGem
Name: greeting
Version: 0.1.0
File: greeting-0.1.0.gem
gemをインストールして実行してみる
docker compose run app sh
をしてgemのインストールと、irbで実行をしてみます。
$ gem install greeting-0.1.0.gem
Building native extensions. This could take a while...
Successfully installed greeting-0.1.0
1 gem installed
$ irb -rgreeting
irb(main):001:0> Greeting.say
=> "Hello, World!"
これで無事Gemを作成することができました!
まだ単純な関数しか実装していないですが、もっと複雑なものも実装してみたいと思います。
今回のサンプルコードはこちらにおいてあります。
Ateam Group U-30 のカレンダー3日目は、Qiita株式会社の @ohakutsu が担当します!
ぜひ、Ateam Group U-30 のカレンダーを購読設定して、明日の記事もご覧いただけると嬉しいです。