LoginSignup
11
2

More than 1 year has passed since last update.

RubyGemsでrust extensionがリリースされたので、Rustでgemを作ってみた

Last updated at Posted at 2022-12-01

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で議論されていますが、今回私が作ったものとディレクトリ構造が変わりそうです。
こちらが正式にマージされた場合はそちらに則って作成してください。

https://github.com/rubygems/rubygems/pull/5613

なぜ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の間の型の変換も行ってくれます。

Cargo.toml
[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のメソッドの追加

src/lib.rs
+ 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行の設定を追加します。

greeting.gemspec
+   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で実行をしてみます。

Docker内
$ gem install greeting-0.1.0.gem
Building native extensions. This could take a while...
Successfully installed greeting-0.1.0
1 gem installed
Docker内
$ irb -rgreeting
irb(main):001:0> Greeting.say
=> "Hello, World!"

これで無事Gemを作成することができました!

まだ単純な関数しか実装していないですが、もっと複雑なものも実装してみたいと思います。

今回のサンプルコードはこちらにおいてあります。


Ateam Group U-30 のカレンダー3日目は、Qiita株式会社の @ohakutsu が担当します!

ぜひ、Ateam Group U-30 のカレンダーを購読設定して、明日の記事もご覧いただけると嬉しいです。

参考

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