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

[Ateam Group U-30 のカレンダー](https://qiita.com/advent-calendar/2022/ateam-group-u30)の2日目はQiitaの @kyntk が担当します。

---

今年、RubyGems 3.3.11でRust extensionsのexperimentalリリースがありました。

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

変更されたPull Requestは↓です。
この変更で`CargoBuilder`が実装され、`Cargo.toml`ファイルを見つけるとcargoでRustのビルドができるようになりました。
今まではRust用にビルドの設定をする必要があったのですが、かんたんにビルドができるようになりました。

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

ちょうど、Qiita社でもRustを使う機会がありそうだったので、勉強がてらRustでgemを作成してみました。

:::note warn
Rust拡張は2022年11月時点でexperimentalです。
仕様が変わる可能性があるのでご注意ください。
:::

```bash
$ gem -v
3.3.25 # larger than 3.3.11
```

:::note warn
また、現時点ではマージされていないですが、scaffoldのコマンドが追加される可能性があります。
下記のPull Requestで議論されていますが、今回私が作ったものとディレクトリ構造が変わりそうです。
こちらが正式にマージされた場合はそちらに則って作成してください。

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


## なぜRustなのか

Rustは実行速度や詳細なメモリ管理などの特徴を持ち高いパフォーマンスを出せるだけでなく、メモリ安全性やデータ競合を防ぐ仕組みを持っています。
またRustはC互換のAPIも持っており、Cとの統合もしやすかったようです。

https://www.rust-lang.org

RubyGemsにはすでにC拡張があり、Cで書かれたコードをGemとしてビルドすることができますが、冒頭のリリースによってRustでもGemを実装することができるようになりました。

## 作ってみた

サンプルコードはこちらにあります。

https://github.com/kyntk/q-mishmash-of-sample-code/tree/main/rust-gem-greeting

実際にはDockerfileで設定していましたが、この記事では順に説明していきます。

まずはgemを3.3.11以上にアップデートします。

```bash
$ gem update --system
$ gem -v
3.3.25
```

そして今回は`greeting`というgemを作っていきます。
rspecやrubocop、ライセンスなどはお好みで設定してOKです。

```bash
$ bundle gem greeting
$ cd greeting
```

初期のgemspecには`TODO`がたくさんあるので、そこを書き換えていきます。

```bash
$ bundle install
$ bundle exec rake
```

gemspecにTODOが残っていると失敗することがありますが、rspecなどのrakeタスクが実行できることを確認します。

## Rustの環境構築

rustupというツールを使ってRustの環境を構築します。
cargoはRustのパッケージマネージャーです。

https://www.rust-lang.org/tools/install

```bash
$ 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は勉強中なので、このコード自体はあまり当てにしないでください。

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

https://github.com/matsadler/magnus

```diff_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拡張のときはこの設定が必要とのことです。

https://doc.rust-lang.org/reference/linkage.html

#### Rubyのメソッドの追加

```diff_rust: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行の設定を追加します。


```diff_ruby:greeting.gemspec
+   spec.extensions = ["Cargo.toml"]
+   spec.files = ["Cargo.toml", "Cargo.lock", "src/lib.rs"]
```

`spec.extensions`はgemのビルドのときに、rust extensionのパスを指定するために使います。
C拡張では`extconf.rb`などを指定するプロパティです。

https://github.com/rubygems/rubygems/pull/5175/files#diff-ce6d3f5c5fbac5b09198e9bb2f5d3e48d020d7fd508d4516ceb04089230680baR126-R128

https://guides.rubygems.org/specification-reference/#extensions

### gemのビルド

gem buildコマンドでビルドをします。
先程Cargo.tomlのパスを指定したので、RubyGemsによって自動でCargoBuilderが選択されてcargo buildが実行されます。

ビルドの設定をしていないのにビルドができちゃいました！

```bash
$ 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で実行をしてみます。

```bash: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
```
```ruby:Docker内
$ irb -rgreeting
irb(main):001:0> Greeting.say
=> "Hello, World!"
```

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

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

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

https://github.com/kyntk/q-mishmash-of-sample-code/tree/main/rust-gem-greeting

---

[Ateam Group U-30 のカレンダー](https://qiita.com/advent-calendar/2022/ateam-group-u30)3日目は、Qiita株式会社の @ohakutsu が担当します！

ぜひ、[Ateam Group U-30 のカレンダー](https://qiita.com/advent-calendar/2022/ateam-group-u30)を購読設定して、明日の記事もご覧いただけると嬉しいです。


## 参考

- [Writing a Rust gem from scratch – Brian Kung](https://briankung.dev/2022/02/02/writing-a-rust-gem-from-scratch/)
- [Add cargo builder for rust extensions by ianks · Pull Request #5175 · rubygems/rubygems · GitHub](https://github.com/rubygems/rubygems/pull/5175)
- [実践Rust入門［言語仕様から開発手法まで］](https://gihyo.jp/book/2019/978-4-297-10559-4)
- [RubyGems Guides](https://guides.rubygems.org/specification-reference/)