2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-20

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

Riceとは何?

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

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

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

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

を例にとってRiceがどんな風にプロジェクトに使われているか観察してみます。
まずはgemspecを見てみます。

Gemspec

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_dependencyrice を指定している

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

Rubyのファイル

lib/morph-ruby.rb

# ext
require "morph/ext"

# modules
require "morph/version"

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

extconf.rb

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

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

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

#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のヘッダーファイルを指定しています。

#include <morph/client.h>

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

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

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

extern "C"
void Init_ext() {
}

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

Module rb_mMorph = define_module("Morph");

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

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の場合でしょう。

    .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;
      });

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

Rakefile

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

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さんに感謝します。何かの参考になれば幸いです。

この記事は以上です。

2
1
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?