おことわり
この記事は、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_dependency
でrice
を指定している
といった感じで書かれています。次にlib
以下のRubyのファイルを見てみましょう。
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
を用いて依存するライブラリが存在するかどうかを確認しているようです。C言語の拡張を書くmkmf
ライブラリと同じものだと思われます。またグローバル変数 $CXXFLAGS
を用いることによって、コンパイル時のフラグを指定することができるようです。C++17というのはC++の規格の一つっぽいです。
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さんに感謝します。何かの参考になれば幸いです。
この記事は以上です。