株式会社クリアコード > ククログ

ククログ

タグ:

RubyKaigi 2017 - Improve extension API: C++ as better language for extension #rubykaigi

RubyKaigi 2017で拡張ライブラリー関連の話をしてきた須藤です。クリアコードはシルバースポンサーとしてRubyKaigi 2017を応援しました。

関連リンク:

内容

C++11を活用するともっと拡張ライブラリーを書きやすくなるよ、という内容でした。詳細は事前情報を読んでください。

個人的には今後の拡張ライブラリー開発にプラスになるとても実用的な話をしたつもりだったのですが、あまり反響がなかったので、よさを伝えきれなかったのだと思います。残念。

誰も質問してくれなかったので付録の生のC API以外で拡張ライブラリーを書く方法の比較はお蔵入りになりました。聞きたい人はなにかのイベントに呼んでください。

Red Data Tools

発表の反響はあまりなかったですが、Red Data Toolsの反響はありました。

RubyData Workshop in RubyKaigi 2017の1つとしてSpeee@hatappiさんとRed Data Toolsの紹介をしました。(@hatappiさんがメインで説明・進行をして、私はたまに補足するスタイル。)

来週の火曜日(9月26日)の夜にSpeeeさんで開催するRed Data Toolsの開発イベントOSS Gate東京ミートアップ for Red Data Tools in Speeeの参加者が増えました。オンラインで相談する場所はGitterのred-data-tools/jaにあるので、開発イベントに参加できない人も一緒に開発しましょう!

Rubyでデータ処理できるようにしたいみなさん、一緒に開発していきましょう!

自分達は開発できない・開発する時間がないけどお金は出せるという場合は、クリアコードに開発の仕事を依頼するというやり方があるのでお問い合わせください。

OSS Gate

Red Data Toolsと同じようにOSS Gateも反響がありました。RubyKaigi 2017 前夜祭安川さんが紹介してくれたのと、Speeeさん・永和システムマネジメントさん・ドリコムさん・ピクシブさんのブースにチラシを置いてもらったのが大きいです。ありがとうございました!

おかげで広島でもOSS Gateの活動を始められそうです。Gitterのoss-gate/hiroshimaで相談しているので、広島でもOSSの開発に参加する人が増えるとうれしい人は参加してください。

全国のOSS Gateワークショップ開催情報は次の通りです。近隣で開催している場合はぜひビギナー・サポーターとして参加してください。

まとめ

RubyKaigi 2017の発表内容と成果を紹介しました。

タグ: Ruby
2017-09-22

事前情報:RubyKaigi 2017 - Improve extension API: C++ as better language for extension #rubykaigi

結構Rubyの拡張ライブラリーを書いている方だと思っている須藤です。RubyKaigi 2017で拡張ライブラリー関連の話をする予定です。RubyKaigi 2017で私の話をより理解できるようになるために内容を紹介します。

関連リンク:

背景

たくさんRubyの拡張ライブラリーを書いてきた経験を活かして拡張ライブラリーのC APIをもっとよくできないかについて考えています。バインディングについてはRubyKaigi 2016で紹介したGObject Introspectionベースがよいと思っていますが、バインディングではないただの拡張ライブラリーはC++を活用するのがよさそうだと思っています。なぜC++を活用するのがよいと思うかは私が実現したいことに関わっています。

実現したいこと

私が実現したいことはC/C++のライブラリーを使ってRubyスクリプトを高速化することです。具体的には、xtensorというC++で実装された多次元配列ライブラリーを使ってRubyスクリプトを高速化したいです。

1つ1つの機能に対してバインディングを用意してRubyレベルで組み合わせるやり方もあります。ただ、場合によっては機能を実行する毎にRubyレベルに戻ってくるオーバーヘッドを無視できないことがあります。あると思っています。まだ実際に遭遇したわけではありませんが。

あまりよい例ではありませんが。。。たとえば、GPU上で演算をする機能があって、その機能を実行する毎にGPU上にデータを転送して演算をして演算結果をまた転送しなおすとしたら、オーバーヘッドは無視できません。まぁ、この場合は、拡張ライブラリーで一連の演算をまとめるよりも、必要な間はずっとGPU上にデータを置いておく機能をRubyレベルに用意する方が汎用的でよさそうです。最近、Apache ArrowにGPU上のデータを管理する機能が入ったので、この場合はApache Arrowと連携する機能を用意するのがよさそうです。

C++11を活用するやり方

C/C++で書かれたライブラリーを使った拡張ライブラリーを書くにはRubyが提供するC APIを使います。このC APIは悪くないのですが、Cなので書いているときに書きにくいなぁと感じることがあります。

たとえば、メソッドを定義するときに関数定義とメソッドの登録が離れるのが不便だなぁと感じます。次のようにrb_hello()の定義とrb_define_method()の呼び出しが離れています。

#include <ruby.h>

static VALUE
rb_hello(VALUE self)
{
  return rb_str_new_cstr("Hello");
}

void
Init_hello(void)
{
  VALUE rb_cHello = rb_define_class("Hello", rb_cObject);

  rb_define_method(rb_cHello, "hello", rb_hello, 0);
}

あとは、例外が発生したときにキレイにリソースを開放するためにrb_rescue()rb_ensure()を使うときが面倒です。

他には、RubyのオブジェクトをCの値に変換する各種APIに統一感がないのも地味に使い勝手が悪いです。たとえば、Rubyのオブジェクトをboolに変換するにはRTEST()を使いますし、intに変換するにはNUM2INT()を使います。

C++11以降の最近のC++を使うことで今のC APIをもっと便利にできます。

たとえば、C++11にはラムダ式があります。これを活用することで次のようにdefine_method()で直接メソッドを定義できます。これはExt++というライブラリーを使っています。

#include <ruby.hpp>

extern "C" void
Init_hello(void)
{
  rb::Class("Hello").
    define_method("hello",
                  [](VALUE self) { // ←ラムダ式
                    return rb_str_new_cstr("Hello");
                  });
}

Rubyでdefine_methodを使うと次のような書き方になりますが、少し似ていますね。

class Hello
  define_method(:hello) do
    "Hello"
  end
end

C++11を活用するやり方のメリット・デメリット

このようなC++11を活用するやり方のメリットは次の通りです。

  • より完結に書ける
    • ラムダ式:その場で関数を定義できる
    • auto:型推論を使うことで必要な型だけ書けばすむようになる
    • range-based for loop:従来のfor (int i = 0; i < n; ++i)だけでなく、Rubyのeachのように自分でインデックスを回さなくてもforを使える
  • 既存のRubyのC APIも使える
    • 拡張ライブラリーを書いたことがある人なら徐々に便利APIに移行できる
  • C/C++のライブラリーをそのまま使える
    • (Ruby用のじゃなくてC++用の)バインディングを用意する必要がない
    • たとえば、Rustを使うならバインディングを用意する必要がある
  • デバッグしやすい
    • 普通にGDB/LLDBを使える
  • 最適化しやすい
    • Feature #13434 better method definition in C API」関連のAPIの改良にも使えるかも

簡単に言うと、既存の資産を活用しつつ便利になるよ、という感じです。

一方、デメリットは次の通りです。

  • C++には難しい機能がたくさんあるので油断するとメンテナンスしにくくなる
    • たとえばテンプレート
  • ビルドが遅い
  • C++の例外とRubyの例外は相性が悪い
    • Rubyの例外はsetjmp()/longjmp()で実装されているのでRubyの例外が発生すると、スコープを抜けたC++のオブジェクトのデストラクターが呼ばれない
  • 古い環境だとC++11を使うのが大変
    • たとえば、CentOS 6の標準パッケージのg++では使えない

例外に関してはライブラリーでカバーする方法があるので、基本的にはC++に起因するデメリットになります。

このようなデメリットはあるものの、適切に使えば十分メリットの方が大きくなると思っています。Ruby本体にC++のAPIがあってもいいのではないかと考えていた時期もあったのですが、RubyKaigi 2017の資料をまとめていたら少し落ち着いてきて、今は、もう少し検討してよさそうなら提案しよう、くらいに思っています。

C++11を活用する以外のやり方

以前からもっと便利に拡張ライブラリーを書きたいという人たちがいます。私はC++11を活用するアプローチがよいと思っていますが、他のアプローチも紹介します。

大きく分けて3つのアプローチがあります。

  • Rubyを拡張して拡張ライブラリーも書けるようにする
  • C以外の言語で拡張ライブラリーを書けるようにする
  • C APIを使いつつ便利APIで改良する

最後のアプローチがC++11を活用するアプローチです。

最初の「Rubyを拡張する」アプローチはRubexのアプローチです。Rubyに追加の構文を導入して拡張ライブラリーも書けるようにしようというアプローチです。RubyKaigi 2017で発表があります。

Pythonでは同様のアプローチで成功しているプロダクトがあります。それがCythonです。CythonはPythonでデータ分析をする界隈では広く使われています。(使われているように見えます。)

私はこのアプローチはあまりスジがよくないと感じています。理由は次の通りです。

  • 一見使いやすそうだが結局使いにくいAPI
    • Rubyっぽい構文なのでRubyユーザーにも使いやすいような気がするが、実際はところどころに違いがあって、結局Rubyではない言語なので使いにくさにつながる
    • Rubyっぽく書けるのでCの知識は必要なさそうに思えるが、libffiを使うときのように結局Cの知識は必要になる
    • RubyとCだけでなくRubexの知識も必要になり、結局覚えることは結構多い
  • メンテナンスが大変
    • Rubyが新しい構文を導入したら追従する必要がある
    • Rubyの構文と衝突しないようにRubexを拡張していく必要がある
  • デバッグが大変
    • Rubexが生成したCのコードをベースにデバッグする必要がある

ただ、Cythonが成功している事実と、ちょっとした拡張機能を書く分にはRubyの知識と少しのRubexの知識だけでよい(Cのことはあまり知らなくてよい)という事実があるので、もしかしたらそんなにスジは悪くないのかもしれません。数年後も開発が継続していたら再度検討してみたいです。

2番目の「C以外の言語を使う」アプローチはHelixのアプローチです。Rustで拡張ライブラリーを書けるようにしようというアプローチです。RubyKaigi 2017で発表があります。

私はC/C++のライブラリーを使いたいのでこのアプローチは私の要件にはマッチしないのですが、高速化のために処理を全部で自分で実装する(あるいはRustのライブラリーを活用して実装する)場合はマッチしそうな気がします。

このアプローチのメリットは、Rustを知っているならCで書くよりもちゃんとしたプログラムをすばやく書けることです。デメリットはRubyのC APIのフル機能を使えない(使うためにはメンテナンスを頑張る必要がある)ことです。たとえば、Ruby 2.4からrb_gc_adjust_memory_usage()というAPIが導入されましたが、Rustからこの機能を使うためにはバインディングを用意する必要があります。つまり、RubyのC APIの進化にあわせてメンテナンスしていく必要があります。

C++を活用する方法

最後に現時点でC++を活用する方法を紹介します。

1つがRiceを使う方法です。RiceはC++で拡張ライブラリーを書けるようにするライブラリーです。10年以上前から開発されています。C++でPythonの拡張ライブラリーを書けるようにするBoost.Pythonに似ています。

例外の対応やメソッドのメタデータとして引数のデフォルト値を指定できるなど便利な機能が揃っています。ただし、昔から開発されているライブラリーで現在はメンテナンスモードなため、C++11への対応はそれほど活発ではありません。メンテナーは反応してくれるので自分がコードを書いて開発に参加するのはよいアプローチだと思います。

もう1つがExt++を使う方法です。Ext++もC++で拡張ライブラリーを書けるようにするライブラリーです。私が作り始めました。RiceはRubyのCのオブジェクトをすべてラップしてC++で自然に扱えるようにするようなAPIです。つまり、できるだけRubyのC APIを使わずにすむようにしたいようなAPIです。私は、もっとC APIが透けて見えるような薄いAPIの方が使いやすいのではないかという気がしているので、その実験のためにExt++を作り始めました。薄いAPIの方が使いやすいのか、結局Riceくらいやらないと使いやすくないのかはまだわかっていません。Red Data Toolsのプロダクトで使って試し続けるつもりです。

まとめ

RubyKaigi 2017で拡張ライブラリーを書きやすくするためにC++がいいんじゃない?という話をします。

おしらせ

去年もスポンサーとしてRubyKaigiを応援しましたが、今年もスポンサーとしてRubyKaigiを応援します。去年と違って今年はブースはありません。懇親会などで見かけたら声をかけてください。拡張ライブラリーに興味のある人と使いやすいAPIについて話をしたいです!

あと、RubyKaigi 2017の2日目の午後に通常のセッションと並行して「RubyData Workshop」というワークショップが開かれる予定です。まだRubyKaigi 2017のサイトには情報はありませんが、時期に情報が載るはずです。このワークショップではPyCallRed Data Toolsの最新情報を手を動かして体験することができます。Rubyでデータ処理したい人はぜひお越しください!

つづき: 2017-09-22
タグ: Ruby
2017-09-11

RubyKaigiのCFPへの応募例 #rubykaigi

須藤です。関西Ruby会議2017が終わってからRubyKaigi 2017の発表を応募しました。

RubyKaigi 2017CFPがでています。CFPとはもともと(?学会の文脈で)はCall For Papersの略ですが、RubyKaigiの文脈ではCall For Proposalsの略で、「RubyKaigiでの発表を募集しています」という意味です。RubyKaigi 2017の発表の応募は6月17日まで受け付けています。

応募する人が増えるといいなぁと思うので、実際の応募例として私のRubyKaigi 2015からRubyKaigi 2017の分の応募内容を紹介します。どうしてRubyKaigi 2015の分からかというと、CFPアプリケーションができて応募の記録が残るようになったのがRubyKaigi 2015からだからです。

項目

実際の応募内容を紹介する前に、どのような項目を書く必要があるかを紹介します。

  • Title: 発表のタイトル(60文字以下。英語。)
  • Abstract: 発表の概要(600文字以下。英語。)
  • Details: 発表の詳細(日本語可日本語でも検討してもらえるが、RubyKaigiは国際カンファレンスなので英語が望ましい。)
  • Pitch: どうして自分がRubyKaigiでこの発表をするべきなのかの説明(日本語可日本語でも検討してもらえるが、RubyKaigiは国際カンファレンスなので英語が望ましい。)

TitleにはRubyKaigiに参加する人が「どの発表を聞くか」をパッと選ぶときに参考になりそうなものを書きます。

AbstractにはRubyKaigiに参加する人が「どの発表を聞くか」を検討するときに参考になりそうな説明を書きます。

Detailsには発表を選考する人が「どの発表を選ぶか」を検討するときに参考になりそうな発表の説明を書きます。発表を聞いた人が何を得られる発表なのかを書くとよいでしょう。

Pitchには発表を選考する人が「どの発表を選ぶか」を検討するときに参考になりそうな発表者の説明を書きます。たとえば、「私が実装した人なので私が一番詳しいです。なので、私が話すのが一番いいんです!」ということを書きます。

これらのことは応募フォームの説明にも書いているので、応募するときには説明をちゃんと読んで応募内容がずれていないか確認するとよいでしょう。

応募の書き方については以下の情報が参考になります。

明示的に書かれてい情報をインターネット上に見つけられませんでしたが、RubyKaigiは「Ruby」の「Kaigi」なので、Rubyで作られたなにかの発表よりもRubyそのものの発表の方がRubyKaigiにあっていそうです。

応募例:RubyKaigi 2015

RubyKaigi 2015での応募例です。この応募は採択されました。

発表内容:The history of testing framework in Ruby

Title:

The history of testing framework in Ruby

Abstract:

This talk describes about the history of testing framework in Ruby.

Ruby 2.2 bundles two testing frameworks. They are minitest and test-unit. Do you know why two testing frameworks are bundled? Do you know that test-unit was bundled and then removed from Ruby distribution? If you can't answer these questions and you're interested in testing framework, this talk will help you.

This talk describes about the history of bundled testing framework in Ruby and some major testing frameworks for Ruby in chronological order.

Details:

このトークは参加者が「自分にとって適切なテスティングフレームワークを選ぶための知識」を持ち帰れることを目標とします。

テスティングフレームワークはRubyで開発する上で重要な役割を持つソフトウェアです。Rubyを使うと非常に柔軟にプログラムを書くことができるため、意図せずに既存の挙動を変えてしまうことが起こしやすいです。自動化されたテストも一緒に開発すると、できるだけ低いコストでその問題の発生を抑えつつ、楽しくプログラムを書きやすくなります。それを支援するのがテスティングフレームワークです。

テスティングフレームワークはRubyで楽しくプログラムを書くために重要な役割にも関わらず、自分にとって適切なテスティングフレームワークを選ぶという活動はあまり行われていないように感じています。多くの人が使っているから、というのも選んだ理由としては妥当な理由ではあるのですが、流行が変わったとき・テスティングフレームワークのAPIが変わったときなど、今までと状況が変わったときに適切な対応を取りづらくなります。大きな流れが1つではなくなるからです。

自分で「どうしてこのテスティングフレームワークを選んでいるのか」を理解していれば、たとえ状況が変わったときでも適切な基準で適切なフレームワークを選びなおすことができるでしょう。

このトークではRubyにバンドルされたテスティングフレームワークの歴史といくつかの主要なRuby用のテスティングフレームワーク(RSpec, minitest, test-unitと関連ソフトウェアをいくつか)の歴史について時系列で紹介します。歴史を知ることで、それぞれのテスティングフレームワークが何を重視しているのか、そして、それが自分が大事にしていることと合致するのかを判断する材料になるはずです。これにより、参加者が「自分にとって適切なテスティングフレームワークを選ぶための知識」を持ち帰ることの実現を目指します。

Rubyにバンドルされたテスティングフレームワークの歴史については↓にまとめたものをベースとします。 http://www.clear-code.com/blog/2014/11/6.html

Pitch:

最新のRubyである2.2でtest-unitがバンドルされた背景(*)を知る人は少ないでしょう。RubyKaigiでのトークとして、RubyのユーザーがRubyの開発の歴史(の一部)を知る機会があるのは妥当だと考えます。

(*) Test::Unit互換APIを提供するため。そうしないと既存のテストが動かなくなってRubyのバージョンアップの障害になる人がでてバージョンアップの障害になるかもしれない。Python 3のように新しいバージョンがでても古いバージョンを使い続けるユーザーがたくさんいる状況は開発チームとしてはうれしくないので避けたい。

私はRubyのテスティングフレームワークの歴史に最初から関わっているわけではありません。Ruby 1.8と1.9の間くらいからだけです。しかし、その頃からRubyのテスティングフレームワークまわりについては観測してきましたし、このトークで登場するtest-unit gemテスティングフレームワークの現在のメンテナーです。そのため、私はこのトークをする人として適切だと考えます。

応募例:RubyKaigi 2016

RubyKaigi 2016での応募例です。この応募は採択されました。

発表内容:How to create bindings 2016

Title:

How to create bindings 2016

Abstract:

This talk describes how to create Ruby bindings of a C library. I want to increase Ruby bindings developers. If you're interested in using C libraries from Ruby and/or using Ruby more cases, this talk will help you.

This talk includes the following topics:

  • List of methods to create Ruby bindings of a C library.
  • Small example of each method.
  • Pros and cons of each method.

Details:

このトークの目標は参加者が「自分のユースケースにあったRubyバインディングの作り方を知ること」です。まったく知らないという参加者でもわかるようにします。「昔は知っていたけど最近のことはわからない」という参加者でも、2016年時点での最新情報を提供することで、得られるものがあるようにします。

多くのRubyistはRubyバインディングを作る機会はありませんし、作る必要もありません。しかし、作り方を知っていればいざというときにRubyバインディングを作るという選択肢が生まれ、よりRubyを活用できる可能性が高くなります。

Cライブラリーと連携できることはRubyのよいところの1つです。Cライブラリーと連携できると、Rubyスクリプトを高速化したり、既存のCライブラリーの機能を利用したりできます。速さが足りなくてRubyを使えない、機能が足りなくてRubyを使えない、そのようなときの解決策としてRubyバインディングの作り方が役に立ちます。

このトークではRubyバインディングの作り方として次の方法を紹介します。

  • RubyのC APIを使ったRubyバインディングの作り方
  • SWIGを使ったRubyバインディングの作り方
  • libffiを使ったRubyバインディングの作り方
  • GObject Introspectionを使ったRubyバインディングの作り方

それぞれについて具体的なコードとメリット・デメリットを紹介します。この情報をもって「自分のユースケースに適切なCライブラリーのRubyバインディングの作り方を知ること」の実現を目指します。

参考URL:スクリプト言語の拡張機能の作り方とGObject Introspectionの紹介

Pitch:

多くのRubyistはRubyバインディングを作る機会はありませんし、作る必要もありません。しかし、作り方を知っていればいざというときにRubyバインディングを作るという選択肢が生まれ、よりRubyを活用できる可能性が高くなります。

Cライブラリーと連携できることはRubyのよいところの1つです。Cライブラリーと連携できると、Rubyスクリプトを高速化したり、既存のCライブラリーの機能を利用したりできます。速さが足りなくてRubyを使えない、機能が足りなくてRubyを使えない、そのようなときの解決策としてRubyバインディングの作り方が役に立ちます。

以上より、Rubyを活用する機会を増やすため、RubyKaigiでRubyバインディングを作る方法を知る機会があるのは妥当だと考えます。

私は10個以上のRubyバインディングを作った経験があり、このトークで紹介するすべての作り方を使ってきました。そのため、私はこのトークをする人として適切だと考えます。

応募例:RubyKaigi 2017

RubyKaigi 2017での応募例です。この応募が採択されるかどうかはわかりません。

Title:

Improve extension API: C++ as better language for extension

Abstract:

This talk proposes better extension API.

The current extension API is C API. In the past, some languages such as Rust (RubyKaigi 2015), Go (Oedo RubyKaigi 05), rubex (RubyKaigi 2016) were proposed as languages to write extension.

This talks proposes C++ as a better language for writing extension. Reasons:

  • C++ API can provide simpler API than C API.
  • C++ API doesn't need C bindings because C++ can use C API including macro natively. Other languages such as Rust and Go need C bindings.
  • Less API maintenance cost. Other approaches need more works for Ruby evolution such as introduces new syntax and new API.

Details:

このトークの目標は参加者が「現在のC APIの課題を知ること」と「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」です。前者に関しては拡張ライブラリーを書いたことがない人でもわかるようにします。後者に関しては拡張ライブラリーを書いたことががない人には判断できないでしょう。よって、このトークのメインターゲットは拡張ライブラリーを書いたことがある人です。この人たちはこのトークでよりよい拡張ライブラリーのAPIについての知見を得るはずです。拡張ライブラリーを書いたことがない人たちは拡張ライブラリーのAPIそのものについての知見が増えているはずです。

このトークではC APIの課題として次のことを説明します。

  • 関数のシグネチャーを2回書かないといけず、それらがズレていてもコンパイル時に発見できないため、実行時にクラッシュしてしまう可能性が高まる。
  • 冗長である。たとえば、メソッドを登録するときにCの関数を定義してそれを別途登録するケース、eachbegin rescueを実現するためにCの関数を定義するケース、などである。
  • CのデータとRubyのデータ間の変換が面倒であり、入力チェックのコードが多くなってコードの見通しが悪くなり、メンテナンス性が下がる。
  • Rubyは例外が発生するとlongjumpするので、例外発生前に動的に確保したメモリーの開放処理が煩雑になる。

それぞれについて具体的なコードを紹介します。たとえば、関数のシグネチャーを2回書かないといけないとは次のようなコードのことです。

static VALUE
rb_sample_hello(VALUE self)
{
  return rb_str_new_static("Hello");
}

void
Init_sample(void)
{
  VALUE sample = rb_define_class("Sample", rb_cObject);
  rb_define_method(sample, "hello", rb_sample_hello, 0);
}

このコードではhelloメソッドの引数の数が0であることを示すために、rb_sample_hello(VALUE self)と関数を定義し、それをRubyに伝えるためにrb_define_method(..., 0)を実行しています。引数の数が増えた時は両方更新する必要があります。片方の更新を忘れるとクラッシュします。

このような情報をもって「現在のC APIの課題を知ること」の実現を目指します。

それぞれの課題について、課題を解決するC++ APIを提案します。APIだけでなくどうして解決できるのか、および、実装も説明します。このAPIとその解説および実装の説明により「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現を目指します。

たとえば、関数のシグネチャーを2回書かないといけないケースについては、次のようなC++ APIを提案します。

void
Init_sample(void)
{
  rb::Class("Sample", rb_cObject).
    define_method("hello", [](VALUE self) {return rb_str_new_static("Hello");});
}

このAPIではC++のラムダ式[](VALUE self)から引数が0であることがわかるため、その情報を自動的にRubyにも伝えます。これにより、シグネチャーの不整合によるクラッシュを防ぐことができます。[](const char *self)のようにシグネチャーが不正な時はコンパイル時にエラーにすることもできます。

このような情報をもって「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現を目指します。

また、RustやGoやrubexなど他のアプローチについても紹介し、それらのアプローチの課題も紹介します。

たとえば、RustやGoを使うアプローチではC APIのマクロの扱いに課題があります。これらの言語ではCのマクロを扱えないため、RSTRING_LEN()のようなC APIを使うためにはそれぞれの言語で同様の処理を実現する必要があります。rubexのアプローチではrubexという独自の言語の文法を覚える必要があるという学習コスト面と、rubex自体がRuby本体の進化(たとえば文法の追加)に追従しなければいけないというメンテナンス面の課題があります。

このような情報も「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現で役立つはずです。

もしかしたら、参考情報としてRubyのC++ APIを提供するRiceも紹介します。RiceはBoost.PythonのようなAPIを提供します。単なる拡張ライブラリーではなくバインディングを書くときにより便利です。「このトークで提案するC++ APIがよいかどうかを判断する材料を十分に得ること」の実現に役立つ情報なはずです。

Pitch:

Rubyは3.0で高速化を目指しています。Rubyが高速になることで、より速度を期待するユーザーが増えると予想します。Rubyレベルの高速化で満足できるユーザーも多いでしょうが、さらなる高速化を期待するユーザーも増えるはずです。

そうなったとき、より簡単に拡張ライブラリーを書けるようにすることでより簡単にRubyスクリプトを高速に実行できるようになります。そうなることで、そのようなユーザーも楽しくRubyを使えます。楽しくプログラムを書ける人が増えることはRubyのポリシーとあっています。

このトークで紹介する既存のアプローチは私のアイディアではありませんが、提案するC++ APIの部分は私のアイディアです。

私は10個以上の拡張ライブラリーを作った経験があり、C APIのよいところも課題も知っています。そのため、私はこのトークをする人として適切だと考えます。

まとめ

RubyKaigiの発表の応募例として私のここ3年の応募内容を紹介しました。RubyKaigi 2017の発表の応募は6月17日まで受け付けています。ぜひ応募してみてください。

タグ: Ruby
2017-06-06

関西Ruby会議2017:株式会社クリアコード #kanrk2017

須藤です。今年2回目の大阪です。(今年1回目の大阪はOSS Gate大阪ワークショップ2017-02-25でした。)

2017年5月27日に関西Ruby会議2017が開催されました。「Ruby Community and Ruby Business」がテーマということだったので、会社の話をするのがいいだろうなぁと思い、「株式会社クリアコード」というタイトルで基調講演をしました。

関連リンク:

内容

「Ruby Community and Ruby Business」というテーマなので、クリアコードは「Ruby Community」と「Ruby Business」を相互に活かしながらクリアコードが大事にしていることを実現している、という話をしました。クリアコードでの「Ruby Community」は「Ruby関連のフリーソフトウェア開発活動」、「Ruby Business」は「クリアコードでの仕事の仕方・作り方」です。クリアコードが大事にしていることは「フリーソフトウェアの推進と稼ぐことの両立」です。

1つのストーリーになった話ではなく、関連する話をたくさん集めた話にしました。具体的には次の話をしました。

  • 問題はupstreamで直す
  • 開発を続けられるコードを書く
  • 相手が想像しなくてもわかるように説明する
  • 楽しく開発する
  • 非難するよりも手を動かす
  • 回避策よりも根本解決
  • 受託開発
  • FLOSSサポート
  • OSS開発支援
  • 仕事の作り方:お客さん探しを頑張らない
  • Apache Arrow
  • 採用

最初の方の話は開発スタイルにまとまっている話です。

テーマに沿った内容になったと思っているのですが、いかがだったでしょうか?

まとめ

関西Ruby会議2017で基調講演をしてきました。「株式会社クリアコード」というタイトルでフリーソフトウェア開発と会社の話をしました。

この話を読んで仕事が決まる・採用の応募があるとすごく話がキレイにまとまるので、関西Ruby会議2017ではタイミングを逃してしまって話しかけられなかったという人は、遠慮せずにまだ間に合うのでご連絡ください。

タグ: Ruby | 会社
2017-05-29

DataScience.rb ワークショップ 〜ここまでできる Rubyでデータサイエンス〜:RubyもApache Arrowでデータ処理言語の仲間入り #datasciencerb

須藤です。最近気になるApache Arrowのissueは[ARROW-1055] [C++] Create add-on library for CUDA / GPU integrationです。

2017年5月19日に開催されたDataScience.rb ワークショップ 〜ここまでできる Rubyでデータサイエンス〜で「RubyもApache Arrowでデータ処理言語の仲間入り」という話をしました。このイベントはしまねソフト研究開発センタースポンサーのプロジェクトとRubyアソシエーションスポンサーのプロジェクトの成果発表会のような位置付けだと思うのですが、私がApache Arrowの紹介をしたいと言ったので、それらとまったく関係のないApache Arrowの話をねじ込んでもらいました。ありがとうございます。

関連リンク:

内容

Ruby界隈ではApache Arrowのことを知っている人はあまりいないはずです。その前提で、私の発表ではApache Arrowがなにを目指しているプロジェクトで、現状はどうなっていて、今後はどうなる予定か、ということを紹介しました。

詳細はスライドを参照してもらうとして、ざっくりいうと次のような感じです。

  • Apache Arrowが目指していること:
    • 低いデータ交換コスト
    • 最適化されたデータ処理実装の共有
  • Apache Arrowの現状:
    • 「低いデータ交換コスト」の方はプロダクションで使えそうなくらいになっている
    • 「最適化されたデータ処理実装の共有」の方はこれから
    • Java、C++、Python、Ruby、Lua、Go、JavaScript間で低コストでデータ交換できる
  • Apache Arrowの今後:
    • Apache Arrowを使ったデータ交換の推進(たとえば、SparkとPySpark間でのデータ交換にApache Arrowを使う変更がマージされて現実に使っていく。似たようなことをいろいろなプロジェクトで進めていく。)
    • 最適化されたデータ処理実装の提供
    • Apache Arrow対応言語の増加

どうしてRubyがApache Arrowに対応するとデータ処理できるようになるかを説明するために、まず、Apache Arrowがでてきた背景を説明します。

現在の(ビッグデータの)データ分析システムは1つの大きなソフトウェアでなんでもかんでも全部やるというやり方ではなく、複数のシステムが協調してデータ分析するというやり方になっています。たとえば、データの管理はHadoopでデータの分散処理基盤はSparkでユーザーがカスタマイズする部分はPythonで、といった具合です。

複数のシステムが協調するためには分析対象のデータを相互にやりとりする必要があります。データが大量にあるのでデータ交換のコストがどのくらいかは重要です。コストが高いと本来やりたかったデータ分析処理に時間を使えずにデータ交換ばかりに時間を使ってしまうからです。現状はコストが高くてそのような状況になりがちでした。それを解決するためにでてきたのがApache Arrowです。

Apache Arrowはデータのシリアライズ・デシリアライズコストをほぼ0にします。そのため、データ交換のコストの多くはデータの送受信くらいになります。別マシン間でのやりとりであればネットワーク上での通信処理、同一マシン間でのやり取りであればメモリー上へのデータの読み書き処理が主なコストになるので、かなり高速です。

データ交換コストが低くなると、データ分析システムの一部にRubyを使いやすくなります。これまでは、データ交換コストが高いので、できるだけサブシステム間でのデータ交換を減らしたくなりましたが、コストが低いとカジュアルにデータ交換できます。そうすると、「この処理はRubyが得意だからこの処理だけRubyでやろう」ということがしやすくなります。Rubyでできることが増えていくともっとRubyの活躍の場が増えます。

RubyがApache Arrowに対応するとRubyにデータが回ってくるようになるのでデータ処理できるようになるということです。

というのが私の目論見ですが、このようになるといいなぁと思ってRubyでApache Arrowを使えるようにする作業を進めています。現在の状況は次の通りです。

  • Apache ArrowはRubyを公式サポート
    • 私が開発した成果はApache Arrow本家に取り込まれました。継続的な活動を認められてコミッターにもなりました。
  • RubyからApache Parquet・Apache Arrow・Feather形式のデータを読み書き可能
    • Apache ParquetはHadoop界隈でよく使われている形式です。Hdoop方面のデータを読み書きできるということです。Apache ArrowのC++実装とApache ParquetのC++実装が連携しているので、それらの成果を利用しています。
    • FeatherはR界隈でよく使われている形式です。Rで処理したデータをRubyで読み込んだり、Rubyで収集・加工したデータをRに渡したりできます。
  • Apache Parquet・Apache Arrow・Feather形式のデータとRuby/GSL、NMatrix、Numo::NArrayのデータを相互変換可能
    • Ruby/GSL、NMatrix、Numo::NArrayはRuby用の行列演算ライブラリーです。読み込んだデータをこれらのライブラリーのオブジェクトに変換すればそれらの機能でデータを処理できます。
    • サンプルコードはkou/rabbit-slide-kou-data-science-rb/sample/にあります。
  • Apache Arrow形式のデータとGroongaのデータを相互変換可能
    • Groongaは集計機能も得意な全文検索ライブラリーです。RubyにはよくできたGroongaのバインディングがあるので、Groongaを使って高速にデータ処理することができます。たとえば、ログメッセージを全文検索して対象のログを絞り込んだり、全文検索インデックス内の統計情報を活用して重要語のみを抽出したりできます。
    • GroongaはApache Arrowに対応している(Groonga自体にApache Arrow形式のデータを読み書きする機能がある)ので、Rubyを経由せずに相互変換できて高速です。

今後ですが、1人でやっていては限界があるので、Red Data Toolsプロジェクトとして「Rubyでデータ処理したい!」という人たちと一緒に活動していく予定です。このプロジェクトは次のようなポリシーで活動していくので、賛同できる人はぜひ一緒にRubyでデータ処理できるようにしていきましょう!参加する人はチャットで参加表明してください。なにから着手するか相談しましょう。

  1. Rubyコミュニティーを超えて協力する
    • 私たちはRubyコミュニティーとも他のコミュニティーとも協力します。たとえば、私たちは多くの言語が共通で使っているApache Arrowを使いますし、Apache Arrowの開発に参加して開発成果を共有します。
  2. 非難することよりも手を動かすことが大事
    • 私たちは現状(RubyよりもPythonの方がたくさんよいツールが揃っているかもしれません)や既存ライブラリーの実装を非難することなどに時間を使うよりも、コードを書いたりテストをしたりドキュメントを書いたり私たちの活動を紹介したり他のプロジェクトにフィードバックをしたりといったことに時間を使います。
  3. 一回だけの活発な活動よりも小さくてもいいので継続的に活動することが大事
    • Rubyでたくさんのデータ処理をできるようになるために私たちはたくさんやることがあるかもしれません。データ処理のためのすばらしいツール群一式を揃えるために継続的に活動する必要があります。そのため、1回だけの活発な活動よりも継続的な活動の方が大事です。
  4. 現時点での知識不足は問題ではない
    • 私たちは高速なツールを実装するために数学や統計学や線形代数学などの知識が必要かもしれません。しかし、このプロジェクトに参加する時点でそれらの知識は必須ではありません。なぜなら活動をしていく中で学んでいくことができるからです。私たちは既存の高速な実装を使ったり、既存の高速な実装から学んだりすることができます。
  5. 部外者からの非難は気にしない
    • 私たちがデータ処理のためのすばらしいツール群一式を揃えるまでに時間がかかるかもしれません。そうなるまでは部外者が私たちの活動を非難するかもしれません。私たちはそれらを気にしません。私たちにはそれらを処理している時間はありません。 :-)
  6. 楽しくやろう!
    • Rubyを使っているんですから!

まとめ

Rubyでデータ処理できるようになるために役立ちそうなApache Arrowの紹介と現在できることを紹介しました。また、これからさらにRubyでデータ処理できる状況を推進していくために、Red Data Toolsプロジェクトを立ち上げました。Red Data Toolsプロジェクトに参加する人はチャットで参加表明してください。

「Rubyでデータ処理できるようにしよう!」活動を推進するために、開発費を提供するという方法もあります。手は動かせないけどお金は出せるという方はご連絡ください。

Apache Arrowは去年のはじめに始まったばかりの新しいプロジェクトなので、まだまだ知らない人も多いはずです。Apache Arrowが早く広まってデファクトスタンダードになると「Rubyでデータ処理」という文脈でもうれしいので、Rubyに関係あろうがなかろうが、Apache Arrowのイベントを開催したい場合はぜひお声がけください。日本でのApache Arrowの第一人者(?少なくともApache Arrowの日本人コミッター第1号)として協力できるはずです。

ちなみに、今度の日曜日(2017年5月28日)に大阪でApache Arrowオンリーイベントがあります。関西在住の方はぜひお越しください。現時点で定員オーバーですが、これからキャンセルが増えると思うので参加できるのではないかと思います。

つづき: 2017-06-12
タグ: Ruby
2017-05-22

名古屋Ruby会議03:Apache ArrowのRubyバインディングをGObject Introspectionで #nagoyark03

2017年2月11日に名古屋Ruby会議03大須演芸場で開催されました。ここで咳さんの並列処理啓蒙活動話の前座の1人として話してきました。内容は、Apache ArrowGObject IntrospectionRroongaを活用すれば自然言語のデータ分析の一部でRubyを活用できるよ!、です。

関連リンク:

リポジトリーには今回のスライド内で使ったスクリプトも入っています。

内容

当初は以下にいろいろまとめていた通り、Apache Arrow・GObject Introspectionはどんな特徴でどういう仕組みでそれを実現しているかといったことも説明するつもりでした。

ただ、内容をまとめていく過程で、特徴や仕組みの説明が多いと大須演芸場で話すには勢いが足りないと判断しました。その結果、詳細をもろもろ省略したストーリーベースの内容にしました。そのため、↑の内容はほぼ使っていません。

ストーリーベースの内容にしたことで「あぁ、たしかにデータ分析の一部でRubyを使えるかも」という雰囲気は伝わりやすくなったはず(どうだったでしょうか?)ですが、詳細の説明を省略したので詳細が気になったまま終わった人もいるかもしれません。そんな人たち向けに詳細がわかる追加情報を紹介します。

Apache ArrowとApache Parquet

まず、Apache ArrowとApache Parquetの連携についてです。両者はどちらもカラムストアのデータについて扱いますが、Apache Arrowはメモリー上での扱い、Aapache Parquetはストレージ上での扱いという違いがあります。この違いのためにトレードオフのバランスが変わっています。詳細はThe future of column-oriented data processing with Arrow and Parquetを見てください。2016年11月にニューヨークで開催されたDataEngConf NYCでのApache Parquetの作者の発表資料です。

ざっくりとまとめると次の通りです。

  • Apache Parquetはストレージ上でのカラムストアのデータの扱いなので、次の傾向がある。
    • CPUを活用するよりもI/Oを減らすほうが重要(たとえば、データを圧縮するとI/Oは減る一方CPU負荷は増えてしまうが、I/Oを減らしたいので圧縮をがんばる)
    • シーケンシャルアクセスが多い
  • Apache Arrowはメモリー上でのカラムストアのデータの扱いなので、次の傾向がある。
    • I/Oを減らすよりもCPUを活用するほうが重要(たとえば、CPUキャッシュミスが少なくなるようなデータ配置にする)
    • シーケンシャルアクセスもあるしランダムアクセスもある

データの配置や高速化の工夫なども前述の発表資料で説明しているので興味のある人は発表資料も確認してください。

Apache ArrowとApache Parquetは連携できます。具体的に言うと、Apache ParquetのC++実装にはApache Arrowのデータを読み書きできる機能があります。これを使うとApache ParquetのデータをApache Arrowのデータとして扱うことができます。

GObject Introspection

GObject Introspectionに関する情報は次の記事を参考にしてください。

GObject Introspectionに対応するとバインディングを書かなくてもCライブラリーのテストをRubyで書けるようになります。Cライブラリーの開発を捗らせるためにGObject Introspectionに対応させるというのもアリです。(これも当初は話したかったけど省略した話題です。)

GObject IntrospectionのRubyバインディングであるgobject-introspection gemについてはここで少し補足します。

話の中で、GI.loadした後、よりRubyっぽいAPIになるように一手間かけるとグッと使いやすくなると説明しました。当日は次のようにエイリアスを使う方法だけを紹介したのですが、別の方法もあってそれを紹介することをすっかり忘れていました。

require "gi"
Arrow = GI.load("Arrow")
class Arrow::Array
  def [](i)
    get_value(i)
  end
end

実はGObjectIntrospection::Loaderには定義するメソッド名を変える機能があります。上述のケースではエイリアスを作るのではなく、最初からget_value[]として定義するとよいです。

class Arrow::Loader < GObjectIntrospection::Loader
  private
  def rubyish_method_name(function_info, options={})
    # 引数が1つで、メソッド名がget_valueならメソッド名を[]にする
    if function_info.n_in_args == 1 and function_info.name == "get_value"
      "[]"
    else
      super
    end
  end
end

このようにGObjectIntrospection::Loaderをカスタマイズするやり方には次のメリットがあります。

  • 余計なメソッドを増やさない(今回のケースではget_value
  • 新しく同じパターンのメソッドが増えてもエイリアスを追加する必要がない(たとえば、Arrow::Array以外にget_value(i)なメソッドが増えてもバインディングを変更する必要がない)

この実装はRed Arrow(RArrowから名前を変更、由来はRubyは赤いからというのと西武新宿線の特急列車)にあります。open {|io| ...}を実現する方法も面白いので、GObject Introspectionが気になってきた方はぜひ実装も見てみてください。

まとめ

名古屋Ruby会議03で「Apache ArrowとGObject IntrospectionとRroongaを使って自然言語のデータ分析の一部でRubyを活用する」という話をしました。(使い方を間違っていましたが)はじめて小拍子を使ったり、はじめてマクラの後に羽織(?)を脱いだりできて、楽しかったです。貴重な経験になりました。声をかけてもらってありがとうございます。名古屋のみなさん(名古屋外からの参加の方も多かったですが)に楽しんでもらえていたなら、とてもうれしいことです。

今回の話では詳細をもろもろ省略しましたが、そのあたりに興味のある方がいたらぜひお声がけください。

また、クリアコードと一緒にRubyでデータ分析できる環境を整備していきたい!という方はぜひお問い合わせください。

つづき: 2017-03-15
タグ: Ruby
2017-02-15

名古屋Ruby会議03:Apache ArrowのRubyバインディング(6) #nagoyark03

前回はGObject Introspectionでのオブジェクトの寿命の扱いについて説明しました。今回は戻り値のオブジェクトの寿命について説明します。実際に動くバインディングはkou/arrow-glibにあります。

戻り値のオブジェクトの寿命

一般的に戻り値のオブジェクトの寿命には次のパターンがあります。

  • 所有権は関数実行側にあるパターン:戻り値のオブジェクトが必要なくなったら戻り値を受け取った側が責任を持って解放する。
  • 所有権は関数側にあるパターン:戻り値を受け取った側は解放してはいけない。

話を単純にするために、戻り値のオブジェクトとして文字列(char *)を考えてみましょう。

strdup(3)(引数の文字列をコピーして返す関数)は前者(所有権は関数実行側にあるパターン)です。strdup(3)の戻り値はstrdup(3)を呼び出した側が責任を持って解放しなければいけません。

char *copy;
copy = strdup("Hello");
free(copy); /* 呼び出し側がfree()する。 */

getenv(3)は後者(所有権は関数側にあるパターン)です。getenv(3)の戻り値はgetenv(3)を呼び出した側が解放してはいけません

char *path;
path = getenv("PATH");
/* free(path); */ /* 呼び出し側がfree()してはダメ! */

これが基本です。

ただし、前者の「所有権は関数実行側にあるパターン」にはもう少しパターンがあります。それは戻り値のオブジェクトがコンテナーの場合です。コンテナーというのは配列・リスト・ハッシュテーブルなどのように複数のオブジェクトを格納するオブジェクトのことです。

コンテナーの場合は、次のパターンがあります。

  • コンテナーの所有権のみ関数実行側にあるパターン
  • コンテナーの所有権もコンテナーの中のオブジェクトの所有権も関数実行側にあるパターン

コンテナーの所有権のみの場合はコンテナーのみを解放します。

GObject Introspectionでの戻り値のオブジェクトの寿命の指定

GObject Introspectionではこれらのそれぞれのパターンを指定できます。指定すると後はGObject Introspection(を使ったバインディング、Rubyならgobject-introspection gem)が適切に寿命を管理してくれます。

パターンはそれぞれの関数のドキュメントで指定します。

次の関数は新しくGArrowArrayオブジェクトを作って返す関数です。新しく作ったオブジェクトの所有権は関数呼び出し側にあります。そのため、「所有権は関数実行側にあるパターン」です。

/**
 * garrow_array_builder_finish:
 * @builder: A #GArrowArrayBuilder.
 *
 * Returns: (transfer full): The built #GArrowArray.
 */
GArrowArray *
garrow_array_builder_finish(GArrowArrayBuilder *builder)
{
  auto arrow_builder = garrow_array_builder_get_raw(builder);
  std::shared_ptr<arrow::Array> arrow_array;
  arrow_builder->Finish(&arrow_array);
  return garrow_array_new_raw(&arrow_array);
}

ポイントは次の箇所です。Returns:が戻り値に関する情報のドキュメントという意味で、(transfer full)が「所有権は呼び出し側にある」という意味です。

/**
 * Returns: (transfer full): The built #GArrowArray.
 */

「所有権は関数側にあるパターン」も見てみましょう。

次の関数はフィールド名を返す関数です。フィールド名はフィールドオブジェクトに所有権があるので関数呼び出し側は解放してはいけません。(gcharはGLibが提供しているchar型です。charと同じです。)

/**
 * garrow_field_get_name:
 * @field: A #GArrowField.
 *
 * Returns: The name of the field.
 */
const gchar *
garrow_field_get_name(GArrowField *field)
{
  const auto arrow_field = garrow_field_get_raw(field);
  return arrow_field->name.c_str();
}

ポイントは次の箇所です。戻り値の型にconstがついている場合はGObject Introspectionは所有権は関数側にあると推測します。そのため、明示的にReturns: (transfer none):と書く必要はありません。

const gchar *

「コンテナーの所有権のみ関数実行側にあるパターン」も見てみましょう。

arrow-glibでは使っていないのでGTK+にある関数を持ってきました。

/**
 * gtk_print_backend_load_modules:
 *
 * Returns: (element-type GtkPrintBackend) (transfer container):
 */
GList *
gtk_print_backend_load_modules (void)
{
  /* ... */
}

ポイントは(transfer container)です。これでコンテナーの所有権のみ関数呼び出し側にある、ということを指定します。

(element-type GtkPrintBackend)はコンテナー内の要素の型を指定しています。)

最後に「コンテナーの所有権もコンテナーの中のオブジェクトの所有権も関数実行側にあるパターン」を見てみましょう。

/**
 * garrow_record_batch_get_columns:
 * @record_batch: A #GArrowRecordBatch.
 *
 * Returns: (element-type GArrowArray) (transfer full):
 *   The columns in the record batch.
 */
GList *
garrow_record_batch_get_columns(GArrowRecordBatch *record_batch)
{
  const auto arrow_record_batch = garrow_record_batch_get_raw(record_batch);

  GList *columns = NULL;
  for (auto arrow_column : arrow_record_batch->columns()) {
    GArrowArray *column = garrow_array_new_raw(&arrow_column);
    columns = g_list_prepend(columns, column);
  }

  return g_list_reverse(columns);
}

ポイントは(transfer full)です。(transfer full)はコンテナーなオブジェクトにもそうでないオブジェクトにも使えるということです。

まとめ

GObject Introspectionでの戻り値のオブジェクトの寿命について説明しました。次回はインターフェイスについて説明します。

つづき: 2017-02-15
タグ: Ruby
2017-02-01

名古屋Ruby会議03:Apache ArrowのRubyバインディング(5) #nagoyark03

前回はGObject Introspectionでのエラーの扱いについて説明しました。今回は戻り値のオブジェクトの寿命について説明します。と、考えていたのですが、書いていたら、間違って「戻り値の」オブジェクトの寿命ではなく「一般的な」オブジェクトの寿命について説明してしまっていました。。。なので、今回は「一般的な」オブジェクトの寿命についての説明で、「戻り値の」オブジェクトの寿命は次回にします。。。実際に動くバインディングはkou/arrow-glibにあります。

Apache Arrowでのオブジェクトの寿命の管理:std::shared_ptr

Apache Arrowはstd::shared_ptrでオブジェクトの寿命を管理しています。バインディングは該当オブジェクトを使っているときは参照を増やして、使わなくなったら参照を減らせばよいです。

GObject Introspectionでのオブジェクトの寿命の管理:GObject

GObject Introspection対応ライブラリーはC言語で実装しますが、オブジェクト指向な機能を使えます。たとえば、カプセル化・継承・ポリモルフィズムなどの機能があります。

これらの機能はGObject IntrospectionがベースにしているGObjectというライブラリーが提供しています。GObject Introspectionでのオブジェクトの寿命の管理はこのGObjectの機能を使います。

GObjectではオブジェクトの寿命はリファレンスカウントで管理します。具体的にはGObjectを継承したクラスを作り、そのオブジェクトに対してg_object_ref()/g_object_unref()を使うことで寿命を管理します。

Apache ArrowのオブジェクトとGObjectのオブジェクトを適切な寿命でなじませるには次のようにします。

  • Apache ArrowのクラスとGObjectのクラスを1対1で対応させる。(例:arrow::ArrayGArrowArrayGObjectベースのクラス)に対応させる。)
  • GObjectのオブジェクトを作るときに対応するApache Arrowのオブジェクトの参照を増やす。
  • GObjectのオブジェクトを解放するときに対応するApache Arrowのオブジェクトの参照を減らす。

Apache ArrowのクラスとGObjectのクラスを1対1で対応させるのは簡単です。そうなるようにクラスを設計すればよいだけだからです。

GObjectのオブジェクトを作るときに対応するApache Arrowのオブジェクトの参照を増やすには、GObjectのオブジェクトのインスタンス変数としてstd::shard_ptr<ARROW_CLASS>なインスタンス変数を用意して、GObjectのコンストラクターで代入します。(GArrowArrayではプロパティーという仕組みを利用していますが、利用せずに直接構造体のメンバーを使ってもよいです。)

GObjectのオブジェクトを解放するときに対応するApache Arrowのオブジェクトの参照を減らすには、GObjectのオブジェクトが解放されるときに関数を呼ぶ仕組みがあるのでそれを利用します。(dispose()finalize()の2つ呼ばれる関数がありますが、この場合はfinalize()を利用します。dispose()は循環参照を解決するために利用します。)

arrow-glibでの実装

GArrowArrayを使って実際の実装を説明します。

まず、arrow::Array(Apache Arrowのオブジェクト)の参照を増やすためのインスタンス変数を用意します。このインスタンス変数は外部から参照できる必要はないのでプライベートな構造体に持たせます。(カプセル化)

typedef struct GArrowArrayPrivate_ {
  std::shared_ptr<arrow::Array> array;
} GArrowArrayPrivate;

G_DEFINE_TYPE_WITH_PRIVATE(GArrowArray, garrow_array, G_TYPE_OBJECT)

#define GARROW_ARRAY_GET_PRIVATE(obj)                                   \
  (G_TYPE_INSTANCE_GET_PRIVATE((obj), GARROW_TYPE_ARRAY, GArrowArrayPrivate))

コンストラクターではstd::shard_ptr<arrow::Array>を受け取ってこのインスタンス変数に設定します。(実際はプロパティーを使っているのでもう少し処理が増えています。)

GArrowArray *
garrow_array_new_raw(std::shared_ptr<arrow::Array> *arrow_array)
{
  /* GARROW_ARRAY()はバリデーションつきでGArrowArray *にキャストするマクロ */
  /* GARROW_TYPE_ARRAYはGArrowArray用のGTypeを返すマクロ */
  /* GTypeはGObjectで型情報を表現するデータ */
  auto array = GARROW_ARRAY(g_object_new(GARROW_TYPE_ARRAY, NULL));
  /* GARROW_ARRAY_GET_PRIVATE()は前述のプライベートなデータ用の
     構造体を返すマクロ */
  auto priv = GARROW_ARRAY_GET_PRIVATE(array);
  /* Apache Arrowのオブジェクトの参照を増やす */
  priv->array = *arrow_array;
  return array;
}

デストラクター(finalize())ではコンストラクターで増やした参照を減らします。

static void
garrow_array_finalize(GObject *object)
{
  auto priv = GARROW_ARRAY_GET_PRIVATE(object);
  /* priv->array.reset()でも同じ */
  priv->array = nullptr;
  G_OBJECT_CLASS(garrow_array_parent_class)->finalize(object);
}

まとめ

GObject Introspectionでの「一般的な」オブジェクトの寿命について説明しました。次回は本当に戻り値のオブジェクトの寿命について説明します。

タグ: Ruby
2017-01-31

Ruby on RailsでGroongaを使って日本語全文検索を実現する方法

MySQL・PostgreSQL・SQLite3の標準機能では日本語テキストの全文検索に難があります。MySQL・PostgreSQLに高速・高機能な日本語全文検索機能を追加するMroongaPGroongaというプラグインがあります。これらを導入することによりSQLで高速・高機能な日本語全文検索機能を実現できます。詳細は以下を参照してください。

また、データはMySQL・PostgreSQL・SQLite3に保存して日本語全文検索機能は別途全文検索エンジンGroongaサーバーに任せるという方法もあります。詳細は以下を参照してください。

ここではMySQL・PostgreSQL・SQLite3を一切使わずに、データもGroongaに保存して日本語全文検索を実現する方法を紹介します。

Groongaにデータも保存して使うメリットは以下の通りです。

  • Groongaのフル機能を使える
  • 検索とデータの取得を一度にできるので速い

一方、デメリットは以下の通りです。

  • MySQL・PostgreSQL・SQLite3を使うだけの場合と比べて学習コストが高い(最初にGroongaのことを覚えないといけない)
  • マスターデータを別途安全に管理する必要がある(Groongaにはトランザクション・クラッシュリカバリー機能がないため)

このデメリットのうち学習コストの方をできるだけ抑えつつGroongaを使えるようにするためのライブラリーがあります。それがgroonga-client-modelです。groonga-client-modelがGroongaを使う部分の多くをフォローしてくれるため利用者は学習コストを抑えたままGroongaを使って高速な日本語全文検索システムを実現できます。

この記事ではRuby on Railsで作ったアプリケーションからGroongaを使って日本語全文検索機能を実現する方法を説明します。実際にドキュメント検索システムを開発する手順を示すことで説明します。ここではCentOS 7を用いますが、他の環境でも同様の手順で実現できます。

Groongaのインストール

まずGroongaをインストールします。CentOS 7以外の場合にどうすればよいかはGroongaのインストールドキュメントを参照してください。

% sudo -H yum install -y http://packages.groonga.org/centos/groonga-release-1.2.0-1.noarch.rpm
% sudo -H yum install -y groonga-httpd
% sudo -H systemctl start groonga-httpd

Rubyのインストール

CentOS 7にはRuby 2.0のパッケージがありますが、Ruby on Rails 5.0.1はRuby 2.2以降が必要なのでrbenvとruby-buildでRuby 2.4をインストールします。

% sudo -H yum install -y git
% git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
% git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
% echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
% echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
% exec ${SHELL} --login
% sudo -H yum install -y gcc make patch openssl-devel readline-devel zlib-devel
% rbenv install 2.4.0
% rbenv global 2.4.0

Ruby on Railsのインストール

Ruby on Railsをインストールします。

% sudo -H yum install -y sqlite-devel nodejs
% gem install rails

ドキュメント検索システムの開発

いよいよ日本語全文検索機能を持ったドキュメント検索システムを開発します。

まずはrails newで雛形を作ります。Active Recordを一切使わないので--skip-active-recordを指定しています。

% rails new document_search --skip-active-record
% cd document_search

Gemfileにgroonga-client-model gemを追加します。

gem 'groonga-client-model'

groonga-client-model gemをインストールします。

% bundle install

検索対象のドキュメントを格納するテーブルとそれを高速に検索するためのインデックスを定義します。定義はdb/schema.grnにGroongaのコマンドの書式で書きます。参考になるドキュメントは後で示すのでまずは実際の定義を確認しましょう。

db/schema.grn:

# ドキュメントを格納するテーブル。キーなし。
table_create \
  --name documents \
  --flags TABLE_NO_KEY
# ドキュメントのタイトルを格納するカラム。
column_create \
  --table documents \
  --name title \
  --flags COLUMN_SCALAR \
  --type ShortText
# ドキュメントの内容を格納するカラム。
column_create \
  --table documents \
  --name content \
  --flags COLUMN_SCALAR \
  --type Text

# 全文検索インデックス用のテーブル。
table_create \
  --name terms \
  --flags TABLE_PAT_KEY \
  --key_type ShortText \
  --normalizer NormalizerAuto \
  --default_tokenizer TokenBigram
# ドキュメントのタイトルと内容を全文検索するためのインデックス。
# Groongaではインデックスはカラムの一種。
column_create \
  --table terms \
  --name documents_index \
  --flags COLUMN_INDEX|WITH_POSITION|WITH_SECTION \
  --type documents \
  --source title,content

以下は参考になるドキュメントです。

作成したテーブル・インデックス定義はgroonga:schema:loadタスクでGroongaに取り込めます。

% bin/rails groonga:schema:load

これでGroongaに検索対象のドキュメントを格納するテーブルができたので対応するモデルを作ります

% bin/rails generate scaffold document title:text content:text

これでDocumentクラスがapp/models/document.rbに生成されます。DocumentオブジェクトはActive RecordのようなAPIを提供するのでActive Recordと同じような感じで使えます。

動作を確認するためにQiitaから検索対象のドキュメントを取得するRakeタスクを作ります

lib/tasks/data.rake:

require "open-uri"
require "json"

namespace :data do
  namespace :load do
    desc "Load data from Qiita"
    task :qiita => :environment do
      tag = "groonga"
      url = "https://qiita.com/api/v2/items?page=1&per_page=100&query=tag:#{tag}"
      open(url) do |entries_json|
        entries = JSON.parse(entries_json.read)
        entries.each do |entry|
          Document.create(title:   entry["title"],
                          content: entry["body"])
        end
      end
    end
  end
end

実行して検索対象のドキュメントを作ります。

% bin/rails data:load:qiita

http://localhost:3000/documentsにアクセスし、データが入っていることを確認します。

Qiitaのデータをロード

ビューにヒット件数表示機能と検索フォームをつけてコントローラーで全文検索するようにします。

検索フォームではqueryというパラメーターに検索クエリーを指定することにします。

@documents@requestに変更してビューで@request.responseとしているのは、コントローラーの時点ではまだGroongaにリクエストを発行せず、ビューで必要になった時点で発行するためです。(Active Recordも同じことをやっていますが、Active Recordはto_aが必要になった時点で暗黙的に行っているのでユーザーが気にすることはありません。groonga-client-modelも同じようにすることができるのですが…長くなるので別の機会に説明します。)

@request.responseとしている理由はもう1つあります。groonga-client-modelとActive Recordで検索結果が違うからです。Active Recordはヒットしたモデルの配列を返しますが、groonga-client-modelはそれだけではなくさらに追加の情報も返します。たとえば、「ヒット数」(@request.response.n_hits)も持っています。SQLでは別途SELECT COUNT(*)を実行しないといけませんが、Groongaでは1回の検索で検索結果もヒット数も両方取得できるので効率的です。

app/views/documents/index.html.erb:


 <h1>Documents</h1>

+<p><%= @request.response.n_hits %> records</p>
+
+<%= form_tag(documents_path, method: "get") do %>
+  <%= search_field_tag "query", @query %>
+  <%= submit_tag "Search" %>
+<% end %>
+
 <table>
   <thead>
     <tr>
@@ -12,7 +19,8 @@
   </thead>

   <tbody>
-    <% @documents.each do |document| %>
+    <% @request.response.records.each do |document| %>
       <tr>
         <td><%= document.title %></td>
         <td><%= document.content %></td>

app/controllers/documents_controller.rb:

@@ -4,7 +4,11 @@ class DocumentsController < ApplicationController
   # GET /documents
   # GET /documents.json
   def index
-    @documents = Document.all
+    @query = params[:query]
+    @request = Document.select.
+      query(@query)
   end

   # GET /documents/1

この状態で次のようにレコード数とフォームが表示されるようになります。

フォームを追加

また、この状態で日本語全文検索機能を実現できています。確認してみましょう。

フォームに「オブジェクト」と日本語のクエリーを入力します。元のドキュメントは100件あり、「オブジェクト」で絞り込んで4件になっています。日本語で全文検索できていますね。

「オブジェクト」で検索

次のようにOR検索もできます。「オブジェクト」単体で検索したときの4件よりも件数が増えているのでORが効いていることがわかります。

「オブジェクト OR API」で検索

全文検索エンジンならではの機能を利用

これで基本的な全文検索機能は実現できていますが、せっかく全文検索エンジンを直接使って検索しているので全文検索エンジンならではの機能も使ってみましょう。

ドリルダウン

まずはドリルダウン機能を使います。ドリルダウンとはある軸に注目して情報を絞り込んでいくことです。例えば、商品カテゴリーに注目して商品を絞り込む(例:家電→洗濯機→ドラム式)、タグに注目して記事を絞り込むといった具合です。

まずは各ドキュメントにタグを付けられるようにしましょう。

タグ用のテーブルを作成し、ドキュメント用のテーブルからそのテーブルを参照するようにします。RDBMSと違い、Groongaは直接他のテーブルを参照する機能があります。

db/schema.grnに以下を追加します。

db/schema.grn:

# タグを格納するテーブル。正規化したタグ名がキー。
table_create \
  --name tags \
  --flags TABLE_HASH_KEY \
  --key_type ShortText \
  --normalizer NormalizerAuto
# 表示用のタグ名。たとえば、タグのキーは「rails」でラベルは「Rails」にする。
column_create \
  --table tags \
  --name label \
  --flags COLUMN_SCALAR \
  --type ShortText

# ドキュメントテーブルにタグテーブルを参照するカラムを追加。
# タグは複数設定できる。
column_create \
  --table documents \
  --name tags \
  --flags COLUMN_VECTOR \
  --type tags

# タグ検索を高速にするためのインデックスカラム。
column_create \
  --table tags \
  --name documents_tags \
  --flags COLUMN_INDEX \
  --type documents \
  --source tags

以下は参考になるドキュメントです。

更新したスキーマをロードします。

% bin/rails groonga:schema:load

タグを作ります

% bin/rails generate scaffold tag _key:string label:string

Qiitaのデータからタグ情報もロードするようにします。Tagを毎回createして大丈夫なのかと思うかもしれませんが、大丈夫です。groonga-client-modelはレコード保存にGroongaのloadコマンドを使っています。このloadコマンドの挙動はupsert(すでに同じキーのレコードがなかったら追加、あったら上書き)なのです。

lib/tasks/data.rake:

@@ -10,8 +10,12 @@ namespace :data do
       open(url) do |entries_json|
         entries = JSON.parse(entries_json.read)
         entries.each do |entry|
+          tags = entry["tags"].collect do |tag|
+            tag_name = tag["name"]
+            Tag.create(_key: tag_name, label: tag_name)
+          end
           Document.create(title:   entry["title"],
-                          content: entry["body"])
+                          content: entry["body"],
+                          tags:    tags)
         end
       end
     end

データベース内のデータを削除してQiitaのロードし直します。

% bin/rails runner 'Document.all.each(&:destroy)'
% bin/rails data:load:qiita

ビューにタグ情報も表示します。コントローラーでoutput_columnsを指定しているのは(参照先の)タグテーブルのラベルカラムも取得するためです。デフォルトではタグテーブルのキーしか取得しないので明示的に指定しています。

app/controllers/documents_controller.rb:

@@ -6,6 +6,7 @@ class DocumentsController < ApplicationController
   def index
     @query = params[:query]
     @request = Document.select.
+      output_columns(["_id", "_key", "*", "tags.label"]).
       query(@query)
   end

app/views/documents/index.html.erb:

@@ -14,6 +14,7 @@
     <tr>
       <th>Title</th>
       <th>Content</th>
+      <th>Tags</th>
       <th colspan="3"></th>
     </tr>
   </thead>
@@ -24,6 +25,13 @@
       <tr>
         <td><%= document.title %></td>
         <td><%= document.content %></td>
+        <td>
+          <ul>
+          <% document.tags.each do |tag| %>
+            <li><%= tag.label %></li>
+          <% end %>
+          </ul>
+        </td>
         <td><%= link_to 'Show', document %></td>
         <td><%= link_to 'Edit', edit_document_path(document) %></td>
         <td><%= link_to 'Destroy', document, method: :delete, data: { confirm: 'Are you sure?' } %></td>

「Tags」カラムにタグがあるのでタグがロードされていることを確認できます。

タグがロードされている

実はすでにタグで高速に検索できるようにもなっています。フォームに「tags:@全文検索」と入力すると「全文検索」タグで絞り込めます。(tags:@...は「tagsカラムの値を検索する」というGroongaの構文です。Googleのsite:...に似せた構文です。)

「全文検索」タグで検索

それではこのタグ情報を使ってドリルダウンできるようにします。

ユーザーにとっては、タグをキーボードから入力して絞り込む(ドリルダウンする)のは面倒なので、クリックでドリルダウンできるようにします

コントローラーには次の2つの処理を追加しています。

  • クエリーパラメーターとしてtagが指定されていたらfilter("tags @ %{tag}", tag: tag)でタグ検索をする条件を追加する。
  • タグでドリルダウンするための情報(どのタグ名で絞りこめるのか、また、絞り込んだらどのくらいの件数になるのか、という情報)を取得する

「タグでドリルダウンするための情報を取得する」とはSQLでいうと「GROUP BY tagの結果も取得する」という処理になります。SQLではGROUP BYの結果も取得すると追加でSQLを実行しないといけませんが、Groongaでは1回のクエリーで検索もヒット数の取得もドリルダウン用の情報も取得できるので効率的です。

app/controllers/documents_controller.rb:

@@ -5,9 +5,18 @@ class DocumentsController < ApplicationController
   # GET /documents.json
   def index
     @query = params[:query]
-    @request = Document.select.
+    @tag = params[:tag]
+
+    request = Document.select.
       output_columns(["_id", "_key", "*", "tags.label"]).
       query(@query)
+    if @tag.present?
+      request = request.filter("tags @ %{tag}", tag: @tag)
+    end
+    @request = request.
+      drilldowns("tag").keys("tags").
+      drilldowns("tag").sort_keys("-_nsubrecs").
+      drilldowns("tag").output_columns(["_key", "_nsubrecs", "label"])
   end

ビューではクリックでドリルダウンできる(タグで絞り込める)ようにリンクを表示します。

app/views/documents/index.html.erb:

@@ -5,10 +5,21 @@
 <p><%= @request.response.n_hits %> records</p>

 <%= form_tag(documents_path, method: "get") do %>
+  <%= hidden_field_tag "tag", @tag %>
   <%= search_field_tag "query", @query %>
   <%= submit_tag "Search" %>
 <% end %>

+<nav>
+  <% @request.response.drilldowns["tag"].records.each do |tag| %>
+  <%= link_to_unless @tag == tag._key,
+                     "#{tag.label} (#{tag._nsubrecs})",
+                     url_for(query: @query, tag: tag._key) %>
+  <% end %>
+  <%= link_to "タグ絞り込み解除",
+              url_for(query: @query) %>
+</nav>
+
 <table>
   <thead>
     <tr>
@@ -27,7 +38,9 @@
         <td>
           <ul>
           <% document.tags.each do |tag| %>
-            <li><%= tag.label %></li>
+            <li><%= link_to_unless @tag == tag._key,
+                                   tag.label,
+                                   url_for(query: @query, tag: tag._key) %></li>
           <% end %>
           </ul>
         </td>

これで次のような画面になります。「全文検索 (20)」というリンクがあるので、「全文検索」タグでドリルダウンすると「20件」ヒットすることがわかります。

タグでドリルダウンできる

「全文検索 (20)」のリンクをクリックすると「全文検索」タグでドリルダウンできます。たしかに20件ヒットしています。

「全文検索」タグでドリルダウン

ここからさらにキーワードで絞り込むこともできます。以下はさらに「ruby」で絞り込んだ結果です。ヒット数がさらに減って3件になっています。

「全文検索」タグでドリルダウンして「ruby」で全文検索

全文検索エンジンの機能を使うと簡単・高速にドリルダウンできるようになります。

キーワードハイライト

検索結果を確認しているとき、キーワードがどこに含まれているかがパッとわかると目的のドキュメントかどうかを判断しやすくなります。そのための機能も全文検索エンジンならではの機能です。

highlight_html()を使うとキーワードを<span class="keyword">...</span>で囲んだ結果を取得できます。

snippet_html()を使うとキーワード周辺のテキストを取得できます。

これらを使ってキーワードをハイライトするには次のようにします。

app/controllers/documents_controller.rb:

@@ -8,7 +8,14 @@ class DocumentsController < ApplicationController
     @tag = params[:tag]

     request = Document.select.
-      output_columns(["_id", "_key", "*", "tags.label"]).
+      output_columns([
+                       "_id",
+                       "_key",
+                       "*",
+                       "tags.label",
+                       "highlight_html(title)",
+                       "snippet_html(content)",
+                     ]).
       query(@query)
     if @tag.present?
       request = request.filter("tags @ %{tag}", tag: @tag)

app/views/documents/index.html.erb:

@@ -33,8 +33,16 @@
   <tbody>
     <% @request.response.records.each do |document| %>
       <tr>
-        <td><%= document.title %></td>
-        <td><%= document.content %></td>
+        <td><%= document.highlight_html.html_safe %></td>
+        <td>
+          <% if document.snippet_html.present? %>
+            <% document.snippet_html.each do |chunk| %>
+              <div>...<%= chunk.html_safe %>...</div>
+            <% end %>
+          <% else %>
+            <%= document.content %>
+          <% end %>
+        </td>
         <td>
           <ul>
           <% document.tags.each do |tag| %>

app/assets/stylesheets/documents.scss:

@@ -1,3 +1,7 @@
 // Place all the styles related to the documents controller here.
 // They will automatically be included in application.css.
 // You can use Sass (SCSS) here: http://sass-lang.com/
+
+.keyword {
+  color: red;
+}

「全文検索」タグでドリルダウンして「ruby」で全文検索した状態では次のようになります。どこにキーワードがあるかすぐにわかりますね。

「全文検索」タグでドリルダウンして「ruby」で全文検索した結果をハイライト

スコアでソート

検索結果の表示順はユーザーが求めていそうな順番にするとユーザーはうれしいです。

Groongaはスコアという数値でどれだけ検索条件にマッチしていそうかという情報を返します。スコアでソートすることでユーザーが求めていそうな順番にできます。

@@ -15,8 +15,12 @@ class DocumentsController < ApplicationController
                        "tags.label",
                        "highlight_html(title)",
                        "snippet_html(content)",
-                     ]).
-      query(@query)
+                     ])
+    if @query.present?
+      request = request.
+        query(@query).
+        sort_keys(["-_score"])
+    end
     if @tag.present?
       request = request.filter("tags @ %{tag}", tag: @tag)
     end
ページネーション

groonga-client-modelは標準でページネーション機能を提供しています。Kaminariと連携することでページネーションのUIもすぐに作れます。

Gemfile:

@@ -53,3 +53,4 @@ end
 gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

 gem 'groonga-client-model'
+gem 'kaminari'

app/controllers/documents_controller.rb:

@@ -27,7 +27,8 @@ class DocumentsController < ApplicationController
     @request = request.
       drilldowns("tag").keys("tags").
       drilldowns("tag").sort_keys("-_nsubrecs").
-      drilldowns("tag").output_columns(["_key", "_nsubrecs", "label"])
+      drilldowns("tag").output_columns(["_key", "_nsubrecs", "label"]).
+      paginate(params[:page])
   end

app/views/documents/index.html.erb:

@@ -2,7 +2,7 @@

 <h1>Documents</h1>

-<p><%= @request.response.n_hits %> records</p>
+<p><%= page_entries_info(@request.response) %></p>

 <%= form_tag(documents_path, method: "get") do %>
   <%= hidden_field_tag "tag", @tag %>
@@ -62,4 +62,6 @@

 <br>

+<%= paginate(@request.response) %>
+
 <%= link_to 'New Document', new_document_path %>

RubyGemsを追加したのでGemfile.lockを更新します。アプリケーションサーバーを再起動することも忘れないでください。

% bundle install

画面の上にはページの情報が表示されます。

ページの情報

画面の下にはページを移動するためのリンクが表示されます。

ページネーション

まとめ

MySQL・PostgreSQL・SQLite3を一切使わずにRuby on Railsアプリケーションで日本語全文検索機能を実現する方法を説明しました。データの保存も取得も検索もすべてGroongaで実現しました。単に全文検索できるようにするだけではなく、ドリルダウンやハイライトといった全文検索ならではの機能の実現方法も紹介しました。

Groongaを使いたいけど学習コストが増えそうだなぁと思っていた人は試してみてください。実際に試してみて詰まった場合や、ここには書いていないこういうことをしたいけどどうすればいいの?ということがでてきた場合は以下の場所で相談してください。

Groongaを用いた全文検索アプリケーションの開発に関するご相談は問い合わせフォームからご連絡ください。

Groonga関連の開発・サポートを仕事にしたい方は採用情報を確認の上ご応募ください。

タグ: Ruby | Groonga
2017-01-27

名古屋Ruby会議03:Apache ArrowのRubyバインディング(4) #nagoyark03

前回はなぜApache ArrowのバインディングをGObject Introspectionで作るとよさそうかについて説明しました。今回からはGObject Introspectionを使ったバインディングの作り方について説明します。実際に動くバインディングはkou/arrow-glibにあります。

基本的な作り方はGObject Introspection対応ライブラリーの作り方を参照してください。今回からはもう少し突っ込んだところを説明します。

今回はエラーの扱いについて説明します。

Apache Arrowのエラー:arrow::Status

Apache ArrowはC++で実装されていますが、エラーの通知は例外ではなくarrow::Statusというオブジェクトを返すことで実現しています。たとえば、arrow::io::FileOutputStream::Open()は次のようになっています。パスにあるファイルを開けなかったらarrow::Statusで理由を返します。

namespace arrow {
  namespace io {
    class FileOutputSTream {
      // When opening a new file, any existing file with the indicated path is
      // truncated to 0 bytes, deleting any existing memory
      static Status Open(const std::string& path, std::shared_ptr<FileOutputStream>* file);
    };
  }
}

GObject Introspectionのエラー:GError

GObject Introspectionを使ってエラーを扱うにはGErrorを使います。

まず、一般的なGErrorの使い方を説明します。

GErrorはエラー情報を表現するオブジェクトで、次の情報を保持します。

  • エラーのグループ(ドメインと呼んでいる)
  • エラーコード
  • エラーメッセージ

エラーのグループはGQuarkで表現します。これはRubyやSchemeで言えばシンボルに相当します。ようは名前が紐付いているIDです。

arrow-glib(現在開発しているApache ArrowのGObject Introspection対応ライブラリー)では次のように定義しています。

arrow-glib/error.h:

#define GARROW_ERROR garrow_error_quark()

GQuark garrow_error_quark(void);

GARROW_ERRORというマクロを用意しているのはそういう習慣(#{名前空間}_#{ドメイン名}という命名規則)だからです。直接garrow_error_quark()を呼ぶAPIとしてもよいですが、習慣に乗ったほうが使う人が使いやすくなるのでマクロを定義することをオススメします。

arrow-glib/error.cpp:

G_DEFINE_QUARK(garrow-error-quark, garrow_error)

G_DEFINE_QUARK()の呼び出しでgarrow_error_quark()を定義しています。ざっくり言うと、g_quark_from_static_string("garrow-error-quark");を実行する関数として定義してくれます。

これでGErrorに設定するエラーのグループを使えるようになりました。

次はエラーコードを用意します。具体的にはenumを用意します。enumの中身はarrow::StatusCodeに対応させています。

arrow-glib/error.h:

/**
 * GArrowError:
 * @GARROW_ERROR_OUT_OF_MEMORY: Out of memory error.
 * @GARROW_ERROR_KEY: Key error.
 * @GARROW_ERROR_TYPE: Type error.
 * @GARROW_ERROR_INVALID: Invalid value error.
 * @GARROW_ERROR_IO: IO error.
 * @GARROW_ERROR_UNKNOWN: Unknown error.
 * @GARROW_ERROR_NOT_IMPLEMENTED: The feature is not implemented.
 *
 * The error code used by all arrow-glib functions.
 */
typedef enum {
  GARROW_ERROR_OUT_OF_MEMORY = 1,
  GARROW_ERROR_KEY,
  GARROW_ERROR_TYPE,
  GARROW_ERROR_INVALID,
  GARROW_ERROR_IO,
  GARROW_ERROR_UNKNOWN = 9,
  GARROW_ERROR_NOT_IMPLEMENTED = 10
} GArrowError;

このenum定義から実行時にenumの名前・値を取得できるようにする情報を自動生成する必要があるのですが、ここでの説明は省略します。

これで以下の情報が揃ったのでGErrorを使うための事前準備は完了です。

  • エラーのグループ(ドメインと呼んでいる)
  • エラーコード

残りの以下は実際にGErrorを使うときに個別に設定します。

  • エラーメッセージ

エラーのグループとエラーコードは次のように使います。エラーメッセージはprintf()のように動的にフォーマットできることを示すためにムダに%dを使っています。

static void
fail_function(GError **error)
{
  g_set_error(error,
              GARROW_ERROR,
              GARROW_ERROR_INVALID,
              "Wrong number of argument: required %d argument",
              1);
}

g_set_error()は他にもいくつか亜種があるので必要に応じて使い分けます。

arrow-glibでの実装

実際の実装では次のようにg_set_error()をラップした関数を使っています。

void
garrow_error_set(GError **error,
                 const arrow::Status &status,
                 const char *context)
{
  if (status.ok()) {
    return;
  }

  g_set_error(error,
              GARROW_ERROR,
              garrow_error_code(status),
              "%s: %s",
              context,
              status.ToString().c_str());
}

次のように使います。

/**
 * garrow_io_file_output_stream_open:
 * @path: The path of the file output stream.
 * @append: Whether the path is opened as append mode or recreate mode.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Returns: (nullable) (transfer full): A newly opened
 *   #GArrayIOFileOutputStream or %NULL on error.
 */
GArrowIOFileOutputStream *
garrow_io_file_output_stream_open(const gchar *path,
                                  gboolean append,
                                  GError **error)
{
  std::shared_ptr<arrow::io::FileOutputStream> arrow_file_output_stream;
  auto status =
    arrow::io::FileOutputStream::Open(std::string(path),
                                      append,
                                      &arrow_file_output_stream);
  if (status.ok()) {
    return garrow_io_file_output_stream_new_raw(&arrow_file_output_stream);
  } else {
    std::string context("[io][file-output-stream][open]: <");
    context += path;
    context += ">";
    garrow_error_set(error, status, context.c_str());
    return NULL;
  }
}

このようにエラーをGErrorで表現しておくと、あとはバインディングレベルでいい感じにしてくれます。RubyならGErrorが設定されたら例外にします。

require "gi"

Arrow = GI.load("Arrow")
ArrowIO = GI.load("ArrowIO")

ArrowIO::FileOutputStream.open("/tmp/nonexistent/xxx", false)
# -> gobject-introspection/loader.rb:110:in `invoke': [io][file-output-stream][open]: </tmp/nonexistent/xxx>: IOError: Failed to open file: /tmp/nonexistent/xxx (Arrow::Error::Io)
#       from gobject-introspection/loader.rb:110:in `block in define_singleton_method'
#       from /tmp/a.rb:6:in `<main>'

まとめ

GObject Introspectionでのエラーの扱いについて説明しました。次回は戻り値のオブジェクトの寿命について説明します。

タグ: Ruby
2017-01-25

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|
タグ: