RubyとC++をつなぐRiceを理解したいのでAnkaneさんのプロジェクトmorph-rubyをながめる

おことわり
　この記事は、C++もRiceもわかっていない人間が適当に書いた記事です。
　正確な情報が知りたい人は原典を当たってくださいな！

## Riceとは何？

RubyとC++をつなぐ方法で一番有名なのがRiceです。最近は、Andrew Kane氏が、積極的にRiceを利用してC++のライブラリのバインディングを作成したり、RubyのC++拡張を作成したりしています。とくにRubyでDeep Learningを実行する最も有名なライブラリであるTorch.rbはC++で書かれており、Riceについてもちょっと触ってみないといけないなと思ったのでした。

## Riceの日本語チュートリアル

Riceの日本語の記事はほとんどないですが、次のページが参考になります。

* [C++で作るRuby拡張](https://qiita.com/S_H_/items/3d0aa25e461223030920)

## Ankaneさんのプロジェクトから、Riceを使ったGemの使い方を学習する

* [morpht-ruby](https://github.com/ankane/morph-ruby/blob/master/morph-ruby.gemspec)

を例にとってRiceがどんな風にプロジェクトに使われているか観察してみます。
まずは[gemspec](https://github.com/ankane/morph-ruby/blob/master/morph-ruby.gemspec)を見てみます。

### Gemspec

```ruby
require_relative "lib/morph/version"

Gem::Specification.new do |spec|
# 中略
  spec.files         = Dir["*.{md,txt}", "{ext,lib}/**/*"]
  spec.require_path  = "lib"
  spec.extensions    = ["ext/morph/extconf.rb"]

  spec.required_ruby_version = ">= 2.5"

  spec.add_dependency "rice", ">= 2.2"
# 中略
end
```

* `spec.files` の中に、`ext`を含める
* `spec.extensions` で `["ext/morph/extconf.rb"]` を指定している
* `spec.add_dependency` で `rice` を指定している

といった感じで書かれています。次に`lib`以下のRubyのファイルを見てみましょう。

### Rubyのファイル

[lib/morph-ruby.rb](https://github.com/ankane/morph-ruby/blob/master/lib/morph-ruby.rb)

```ruby
# ext
require "morph/ext"

# modules
require "morph/version"
```

5行目は、バージョン定数を呼んでいるだけなので、実質的に、`requrire "morph/ext"` だけです。ここでは `ext.so` （共有ライブラリの拡張子はプラットフォームによって異なる）を読み込んでいると思われます。

### extconf.rb

`ext/morph/` を見ると、2つだけファイルがあります。`extconf.rb` と `ext.cpp` です。

`extconf.rb` はMakefileを生成します。`spec.extensions`で指定されているファイルですね。ここでは [`have_library`](https://docs.ruby-lang.org/ja/latest/method/Kernel/i/have_library.html) を用いて依存するライブラリが存在するかどうかを確認しているようです。C言語の拡張を書く`mkmf`ライブラリと同じものだと思われます。またグローバル変数 `$CXXFLAGS` を用いることによって、コンパイル時のフラグを指定することができるようです。C++17というのはC++の規格の一つっぽいです。

```ruby:extconf.rb
require "mkmf-rice"

abort "Missing stdc++" unless have_library("stdc++")
abort "Missing ntl" unless have_library("ntl")
abort "Missing helib" unless have_library("helib")
abort "Missing morph" unless have_library("morph")

$CXXFLAGS << " -std=c++17"

create_makefile("morph/ext")
```

次に、いよいよ本丸の `ext.cpp` を見ます

### ext.cpp

```cpp
#include <morph/client.h>

#include <rice/Array.hpp>
#include <rice/Class.hpp>
#include <rice/Constructor.hpp>
#include <rice/Hash.hpp>
#include <rice/Module.hpp>

using namespace Rice;

extern "C"
void Init_ext() {
  Module rb_mMorph = define_module("Morph");

  define_class_under<morph::Client>(rb_mMorph, "Client")
    .define_constructor(Constructor<morph::Client>())
    .define_method("keygen", &morph::Client::keygen)
    .define_method("set", &morph::Client::set)
    .define_method("flushall", &morph::Client::flushall)
    .define_method("dbsize", &morph::Client::dbsize)
    .define_method("info", &morph::Client::info)
    .define_method(
      "get",
      *[](morph::Client& self, const std::string& key) {
        auto value = self.get(key);
        // TODO fix in C++ library
        return value.empty() ? Nil : String(value.c_str());
      })
    .define_method(
      "keys",
      *[](morph::Client& self, const std::string& pattern) {
        auto keys = self.keys(pattern);
        Array res;
        for (auto &k : keys) {
          // TODO fix in C++ library
          res.push(k.c_str());
        }
        return res;
      });
}
```

C++は読めませんが、眺めているだけでも伝わってくるものはあります。最初に[morphのヘッダーファイル](https://github.com/ankane/morph/blob/master/src/client.h)を指定しています。

```cpp
#include <morph/client.h>
```

次に、Rubyのモジュールやクラスや配列やハッシュを使う用意をしているのだと思われます。

```cpp
#include <rice/Array.hpp>
#include <rice/Class.hpp>
#include <rice/Constructor.hpp>
#include <rice/Hash.hpp>
#include <rice/Module.hpp>
```

その次はよくわかりませんが、おまじないだと考えておいても間違いないでしょう。

```cpp
extern "C"
void Init_ext() {
}
```

そして`Morph`というモジュールを規定し、

```cpp
Module rb_mMorph = define_module("Morph");
```

`Client`というクラスに、コンストラクタを定義したり、種々のメソッドを規定しているのでしょう。

```cpp
define_class_under<morph::Client>(rb_mMorph, "Client")
    .define_constructor(Constructor<morph::Client>())
    .define_method("keygen", &morph::Client::keygen)
    .define_method("set", &morph::Client::set)
    .define_method("flushall", &morph::Client::flushall)
    .define_method("dbsize", &morph::Client::dbsize)
    .define_method("info", &morph::Client::info)
```

引数や戻り値を取るようなメソッドの場合は多少複雑なのでしょう。

ここはおそらく戻り値がstringの場合でしょう。

```cpp
    .define_method(
      "get",
      *[](morph::Client& self, const std::string& key) {
        auto value = self.get(key);
        // TODO fix in C++ library
        return value.empty() ? Nil : String(value.c_str());
      })
```

こちらは戻り値が配列なのでしょう。

```cpp
    .define_method(
      "keys",
      *[](morph::Client& self, const std::string& pattern) {
        auto keys = self.keys(pattern);
        Array res;
        for (auto &k : keys) {
          // TODO fix in C++ library
          res.push(k.c_str());
        }
        return res;
      });
```

こうやって見ていくと、FFIの場合とは違って、Riceではある程度は自分でデータのやり取りを書かなければいけないようですね。一方で、RubyのモジュールやらクラスやらをC++で生成できるということで、これはFFIとはかなり方向性が違うので、頭を切り替えていく必要があります。

## Rakefile

最後にRakefileを見てみます。`rake/extensiontask` を追加すればよいことがわかります。remove_extですが、macOS以外の環境では動かないかも知れませんね。

```ruby
require "bundler/gem_tasks"
require "rake/testtask"
require "rake/extensiontask"

task default: :test
Rake::TestTask.new do |t|
  t.libs << "test"
  t.pattern = "test/**/*_test.rb"
end

Rake::ExtensionTask.new("morph") do |ext|
  ext.name = "ext"
  ext.lib_dir = "lib/morph"
end

task :remove_ext do
  path = "lib/morph/ext.bundle"
  File.unlink(path) if File.exist?(path)
end

Rake::Task["build"].enhance [:remove_ext]
```

## 全体のファイル構成

```
.
├── CHANGELOG.md
├── ext
│  └── morph
│     ├── ext.cpp
│     └── extconf.rb
├── Gemfile
├── lib
│  ├── morph
│  │  ├── ext.so
│  │  └── version.rb
│  └── morph-ruby.rb
├── LICENSE.txt
├── morph-ruby.gemspec
├── pkg
│  └── morph-ruby-0.1.0.gem
├── Rakefile
├── README.md
└── test
   ├── client_test.rb
   └── test_helper.rb
```

こんな感じのファイル構成になっています。信頼と安心のAnkane氏のプロジェクトなので、これをテンプレートにしてやっていけばよさそうです。

徹頭徹尾、他の人のプロジェクトを見てコピペして貼り付けて感想を述べただけの薄いエントリで本当に恐縮です。素晴らしいGemを作り続けているAnkaneさんに感謝します。何かの参考になれば幸いです。

この記事は以上です。