search
LoginSignup
2

posted at

updated at

Organization

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

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 のカレンダーを購読設定して、明日の記事もご覧いただけると嬉しいです。

参考

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
What you can do with signing up
2