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

ククログ

«前月 最新
タグ:

Firefoxで外部アプリケーションを起動するだけのアドオンをGo言語で作る方法と注意点

キーワード:golang, バックグラウンド実行, detach

Firefoxのアドオンは、現在はWebExtensionsというAPI群に基づいて開発するようになっています。 このAPI群には「別のブラウザなどの任意のローカルアプリケーションを直接起動する機能」は含まれておらず、そのようなことをしたい場合にはNative Messagingという仕組みで間接的に実現する必要があります。 これは要するに、「コマンドラインオプションやGUIではなく標準入力から与えられる情報を使って、任意のアプリケーションを起動するランチャー」を開発するということです。 関係を図にすると以下のようになります。

+---------------------+
|       Firefox       |   
|+-------------------+|
||Firefox上のアドオン||
|+-------------------+|
|        ↓↑         |
| <WebExtensions API> |
| (Native Messaging)  |
+--------↓↑---------+
     <標準入出力>
         ↓↑
+---------------------+
|      ランチャー     |
+---------------------+
          ↓
   <システムコール>
          ↓
+---------------------+
|外部アプリケーション |
+---------------------+

このNative Messagingの仕組み自体は、細部を除けばGoogle Chrome用拡張機能での仕組みとほぼ同じ仕様です。 そのためFirefoxとChromeに両対応した実装が既にいくつか存在しており、中には上記のようなランチャーとして振る舞う物もあります。 例えばOpen InというプロジェクトではNode.jsベースで開発されたランチャーアプリケーションの実装を使っています。

この記事では、これと似たような物をGo言語で実装する時の注意点を解説します。

起動したい外部アプリケーションが巻き込みで終了されてしまう問題

Go言語では、外部アプリケーションを起動する方法としてexecパッケージを使うのが一般的です。 このパッケージでは同期実行(外部アプリケーションの終了を待ってから次の処理に進む)のexec.Command().Run()と非同期実行(外部アプリケーションを起動した後、すぐに次の処理に進む)のexec.Command().Start()の2つの機能が提供されています。

例えば、FirefoxのWebページ上のコンテキストメニューに「このページをInternet Explorerで開く」のような項目を追加してそこから別のブラウザを起動するというような場合、exec.Command().Run()でIEを起動するとIEを終了するまでの間ずっとランチャーアプリケーションのプロセスが生き続けることになります。 また、そこから起動されるIEのプロセスはFirefoxから見て孫プロセスという扱いになりますので、うっかりその状態でFirefoxを終了すると、孫プロセスになっているIEまでもがまとめて終了されてしまいます。

ということから、このような場面ではexec.Command().Start()の方を使えばよいと考えられるのですが、実際には期待した通りの結果になりません。 こちらで起動した場合でもIEのプロセスは孫プロセスになってしまい、ランチャーがexec.Command().Start()を実行してIEを起動した後でmain()の最後に到達してプロセスが終了すると(Firefoxがランチャーを終了させると)、やはり孫プロセスのIEまで巻き添えで終了されてしまうのです。

プロセスグループを分けての外部アプリケーションの起動

このような現象が発生するのは、Firefox・ランチャー・孫プロセスとして起動されたIEの全てが同じプロセスグループに属しているからです。 言い換えると、ランチャーが起動する孫プロセスについてプロセスグループを分ければ(プロセスをデタッチすれば)、Firefoxやランチャーが終了した後もIEを動作させ続けられると考えられます。 Go言語の場合、一般的にはこれは以下のようにして実現できます。

...
import (
  "os/exec"
  "syscall"
  "log"
)
...

func Launch(path string, args []string) {
  command := exec.Command(path, args...)

  // Windowsの場合
  command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
  // Linux, maxOS (POSIX)の場合
  // command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

  err := command.Start()
  if err != nil {
    log.Fatal(err)
  }
}

システムコールに渡すパラメータを指定する必要があるため、Windowsとそれ以外の環境とでは書き方が変わってきます。

このようにすることで晴れてプロセスグループが分かれてくれて、ランチャー終了後も外部アプリケーションが生き続けるようになってくれる……と思われたのですが、実際にWindows環境のFirefoxで検証してみたところ、残念ながら期待通りの結果は得られませんでした。

上記のようなコードを使って一般的なコマンドラインアプリケーションとして起動する状態にしたランチャーで試す分には、期待通りの振る舞い(ランチャーの終了後も外部アプリケーションのプロセスが残る)を見せました。 しかし、FirefoxのアドオンからNative Messagingの仕組みを経由して起動されるNative Messaging Hostとしてランチャーを動作させると、依然として外部アプリケーションまで終了されてしまうのでした。

Firefoxに固有の事情

これは、Go言語一般の話や、Google Chromeなどと共通の仕組みとしてのNative Messagingではなく、Firefox固有の事情による現象です。

実はWebExtensionsにおけるNative Messagingの説明に記載がありますが、Windowsにおいてこのようなランチャーから外部のプロセスを起動する際は、CREATE_NEW_PROCESS_GROUPではなくCREATE_BREAKAWAY_FROM_JOBという定数で示されるフラグを指定する必要があります。

Go言語の場合はsyscallモジュールにこの定数の定義が含まれていないため、仕様に基づいて0x01000000という数値を直接記述することになります。

func Launch(path string, args []string) {
  command := exec.Command(path, args...)
  // CREATE_BREAKAWAY_FROM_JOB = 0x01000000
  command.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x01000000}
  err := command.Start()
  if err != nil {
    log.Fatal(err)
  }
}

WebExtensionsの元になっているGoogle ChromeのNative Messagingの仕様では特にこのような指定は必要ないため、ChromeとFirefoxの両方に対応したNative Messaging Hostを開発する際には注意が必要です。

このようにフラグを指定して実行すると、無事期待する通りの結果を得ることができました。

(この情報は本記事の初版に対するフィードバックで教えていただきました。ご指摘ありがとうございます。)

有効だった回避策(Windows向け)

結論から述べると、この問題は以下のようなバッチファイルで解決できました。

@ECHO OFF

start %*

これは、自身にコマンドライン引数として渡された内容をそのままコマンド列として非同期に実行するというバッチファイルです。 ランチャーから外部アプリケーションを起動する際に、直接起動せずこのバッチファイル(※正確にはcmd.exe)を介して起動するという使い方をします。

この時の各ソフトウェア同士の関係を図にすると、以下のようになります。

+---------------------+
|       Firefox       |   
|+-------------------+|
||Firefox上のアドオン||
|+-------------------+|
|        ↓↑         |
| <WebExtensions APi> |
| (Native Messaging)  |
+--------↓↑---------+
     <標準入出力>
         ↓↑
+---------------------+
|      ランチャー     |
+---------------------+
          ↓
   <システムコール>
          ↓
+---------------------+
|    バッチファイル   |
+---------------------+
          ↓
   <startコマンド>
          ↓
+---------------------+
|外部アプリケーション |
+---------------------+

単純なのですが、これによってバッチファイルから先のプロセスが別のプロセスグループに分かれるようになり、FirefoxからNative Messaging Hostとしてランチャーを起動した場合でも、Firefoxの終了後も外部アプリケーションのプロセスが残り続けるようになりました。 また、このような動作をさせる場合、ランチャーでバッチファイルを起動する際にはcommand.SysProcAttrの指定はあってもなくても結果は変わりませんでした。

なお、バッチファイルを使うとなるとcmd.exe(コマンドプロンプト)のウィンドウが一瞬表示されるのではないかという懸念もありましたが、実際には特にそのようなこともなく自然に外部アプリケーションが起動しました。 これも、Firefoxがランチャーを起動する際に与えている何らかの指定の影響ではないかと考えられます。

まとめ

以上、Go言語で実装したランチャーを経由してFirefoxから外部アプリケーションを起動する際に、Firefoxが終了した後も外部アプリケーションを起動した状態のままとするためには、バッチファイルを使うとよいCREATE_BREAKAWAY_FROM_JOBフラグの指定が必要であるという注意点をご紹介しました。

環境によってはバッチファイルの実行自体が制限されている場合もあるかもしれません。実際に動作するかどうか、はあらかじめ確認を取っていただくことを強くお勧めします。

FirefoxのWebExtensions APIはGoogle Chromeの拡張機能向けAPIを参考に設計されています。そのためChrome用拡張機能の開発でのノウハウの多くを流用することができ、何か詰まった時は「Chrome用拡張機能ではどうするのが普通なのか?」という観点で調べれば解決策が見つかることが多いです。

しかしながら両者は完全に同一のものではなく、前述のようなFirefoxに固有の注意事項というものもあります。本記事の初版公開時には、オフィシャルのドキュメントにある注意書きを見落としたまま先入観からChrome用拡張機能向けの情報だけを調査していた結果、肝心な情報に全く辿り着けないという結果となっていました。検索も万能ではない(検索語句がずれていると必要な情報に辿り着けない)ために頼りすぎる事・その時得られた結果を過信しすぎる事のリスク、先入観に囚われずにオフィシャルの情報を丁寧に読む事の重要性を改めて実感した次第です。

タグ: Mozilla
2017-09-01

Zulip & PGroonga Night - PGroonga & Zulip #zpnight

ZulipPGroongaサポートを実装した須藤です。PyCon JP 2017に参加するためにZulipの開発者の1人であるGregさんが来ていたので、日本PostgreSQLユーザ会(JPUG)さんに主催してもらってZulipとPGroongaのイベント「Zulip & PGroonga Night」を開催しました。

なお、GregさんのPyConJP 2017でのトーク「Clearer Code at Scale: Static Types at Zulip and Dropbox」(動画)はPyConJP 2017のベストトークに選ばれました。すごい!

Zulip & PGroonga NightではPGroongaの紹介とZulipの全文検索インデックスの更新方法の紹介をしました。

関連リンク:

背景

ZulipではPostgreSQL標準のtextsearchを使って全文検索を実現しています。textsearchは言語特化型のインデックスを作るため、同時に複数の言語をサポートすることができません。また、日本語を含むアジア圏の言語のサポートが不十分なため、日本語を全文検索できないという問題もあります。

そこで、私はZulipでPGroongaを使えるようにしました。PGroongaは言語特化型のインデックスも言語非依存のインデックスも作れます。言語非依存のインデックスを作れば同時に日本語も英語もいい感じに全文検索できます。

クリアコードがZulipを選んだ理由

私がZulipにPGroongaサポートパッチを送ったのは自分たちが必要だからです。クリアコードではチャットツールとしてZulipを使っています。その前はSkypeを使っていました。お客さんとの連絡でSkypeを使う必要があったため、その流れでなんとなく使っていました。Skypeはあまり活用していませんでした。

クリアコードはフリーソフトウェアを推進したい会社なのでフリーソフトウェアではないSkypeを使っていることをどうにかしたいと考えていました。そこで、いくつかフリーソフトウェアなチャットツールを検討しました。そのうちの1つがZulipでした。Zulipのネックは日本語全文検索できないことでした。ネックがあるので選択肢から外すという考え方もあると思いますが、私たちは、自分たちで日本語全文検索できるようにして使うことにしました。フリーソフトウェアのよいところは自分たちで改良できることだからです。

Zulipの全文検索インデックスの更新方法

Zulipは書き込み時のレイテンシーを小さくしておくために工夫をしています。チャットアプリケーションでは書き込みがすぐに終わることはよい使い勝手に直結するからです。

書き込み時はデータをPostgreSQLに書き込むだけで、全文検索インデックスの更新は別途バックグラウンドで実行します。このためにトリガーとNOTIFYLISTENを使っています。

具体的な実装を簡単に紹介します。興味のある人はZulipのコードを見てください。既存のコードから学習することができることもフリーソフトウェアのよいところです。

zerver_messageがメッセージ(チャットの書き込み)を保存しているテーブルです。この中に全文検索対象のデータを入れるカラムを定義します。メッセージのテキストそのものとは別に定義することがポイントです。search_tsvectorがそのカラムです。(PGroongaを使うときの実装ではなくtextsearchを使うときの実装です。)

CREATE TABLE zerver_message (
  rendered_content text,
  -- ... ↓Column for full text search
  search_tsvector tsvector
);

この全文検索対象のデータを入れるカラムには全文検索用のインデックスを作ります。(この例ではPGroongaのインデックスではなくtextsearchのインデックスを作っています。)

CREATE INDEX zerver_message_search_tsvector
  ON zerver_message
  USING gin (search_tsvector);

このメッセージ用のテーブルにトリガーを設定します。このトリガーはメッセージが追加・更新されたときに「更新ログ」テーブル(fts_update_logテーブル)にメッセージのIDを追加します。

-- Execute append_to_fts_update_log() on change
CREATE TRIGGER
  zerver_message_update_search_tsvector_async
  BEFORE INSERT OR UPDATE OF rendered_content
    ON zerver_message
  FOR EACH ROW
    EXECUTE PROCEDURE append_to_fts_update_log();

メッセージのIDを追加する関数の実装は次のようになります

-- Insert ID to fts_update_log table
CREATE FUNCTION append_to_fts_update_log()
  RETURNS trigger
  LANGUAGE plpgsql AS $$
    BEGIN
      INSERT INTO fts_update_log (message_id)
        VALUES (NEW.id);
      RETURN NEW;
    END
$$;

「更新ログ」テーブルの定義は次の通りです。全文検索インデックスを更新するべきメッセージのIDを入れているだけです。

-- Keep ID to be updated
CREATE TABLE fts_update_log (
  id SERIAL PRIMARY KEY,
  message_id INTEGER NOT NULL
);

これで後から全文検索インデックスを更新するための情報を保存する仕組みができました。通常通りメッセージテーブルを操作するだけで実現できていることがポイントです。こうすることでアプリケーション側をシンプルにしておけます。残りの処理は、後から全文検索インデックスを更新する、です。

この処理のためにNOTIFYLISTENを使います。NOTIFYLISTENしている接続に通知する仕組みです。LISTENしている接続はNOTIFYされるまでブロックします。NOTIFYLISTENを組み合わせることで、ポーリングしなくてもイベントが発生したことに気づくことができます。

今回のケースでは「更新ログが増えた」というイベントに気づきたいです。このイベントが来たら全文検索インデックスを更新したいからです。

そのために、「更新ログ」テーブルにトリガーを追加します。「更新ログ」テーブルにレコードが追加されたらNOTIFYするトリガーです。

-- Execute do_notify_fts_update_log() on INSERT
CREATE TRIGGER fts_update_log_notify
  AFTER INSERT ON fts_update_log
  FOR EACH STATEMENT
    EXECUTE PROCEDURE
      do_notify_fts_update_log();

NOTIFYする関数の実装は次の通りです。この関数を実行すると、fts_update_logというイベントをLISTENしている接続のブロックが解除されます。

-- NOTIFY to fts_update_log channel!
CREATE FUNCTION do_notify_fts_update_log()
  RETURNS trigger
  LANGUAGE plpgsql AS $$
    BEGIN
      NOTIFY fts_update_log;
      RETURN NEW;
    END
  $$;

全文検索のインデックスを更新するSQLはPythonから発行しています。全文検索のインデックスの更新処理は必要なときだけ(更新ログがあるときだけ)実行したいです。必要がないときも更新処理を実行し続けるとムダにCPUを使ってしまうからです。

必要なときだけ処理を実行するために、LISTENでブロックします。ブロックが解除されたら(NOTIFYされたら)必ず更新ログがあるので、処理を実行します。↓には入っていませんが、処理が終わったら次のNOTIFYがあるまでまたブロックする実装になっています。こうすることで必要なときだけ処理を実行できるためムダにCPUを使わずにすみます。

cursor.execute("LISTEN ftp_update_log") # Wait
cursor.execute("SELECT id, message_id FROM fts_update_log")
ids = []
for (id, message_id) in cursor.fetchall():
  cursor.execute("UPDATE zerver_message SET search_tsvector = "
                   "to_tsvector('zulip.english_us_search', "
                               "rendered_content) "
                 "WHERE id = %s", (message_id,))
  ids.append(id)
cursor.execute("DELETE FROM fts_update_log WHERE id = ANY(%s)",
               (ids,))

このような複数プロセスでの待ち合わせを実現するためにRDBMSとは別の仕組みを使うことも多いでしょう。たとえば、RedisのPub/Subを使えるでしょう。別の仕組みを使うと運用が面倒になります。PostgreSQLにはNOTIFY/LISTENがあるので、PostgreSQLを使っていて待ち合わせを実現しなければいけないときはNOTIFY/LISTENを使うことを検討してみてください。

まとめ

ZulipとPGroongaのイベントでZulipとPGroongaの情報を紹介しました。クリアコードはZulipを使っていて、今ではなくてはならないツールになっています。ぜひみなさんもZulipを使ってみてください。

Zulipは基本的に自分たちで運用しますが、運用を任せる選択肢もあります。Zulipの開発チームがクラウドサービスでの提供を進めているのです。オープンソースコミュニティは無料で使えるそうです。興味のある人はzulipchat.comを確認してください。

PGroongaが気になる人は、11月3日開催のPostgreSQL Conference Japan 2017に来てください。日本PostgreSQLユーザ会(JPUG)が主催のPostgreSQLのカンファレンスです。ここでPGroongaの最新情報を紹介する予定です。

タグ: Groonga
2017-09-07

db tech showcase Tokyo 2017 - MySQL・PostgreSQLだけで作る高速でリッチな全文検索システム #dbts2017

この発表のためにmroonga_query_expand()を実装した須藤です。db tech showcase Tokyo 2017で「MySQL・PostgreSQLだけで作る高速でリッチな全文検索システム」という話をしました。一昔前の全文検索システムはそこそこ速く全文検索してキーワードをハイライト表示できれば普通でしたが、最近の全文検索システムはそれだけだと機能不足で、オートコンプリートやクエリー展開や関連エントリー表示機能などがあって普通というくらいにユーザーの要求レベルがあがっています。これは、GoogleやAmazonなど便利な検索サービスに慣れたユーザーが増えたためです。そんな時代の変化に対応できる全文検索エンジンがGroongaです。GroongaはMySQL・MariaDB・PostgreSQLから使えるためSQLを使って簡単にイマドキの全文検索システムを実装できます。しかも、運用も楽です。そんな話です。

関連リンク:

内容

まず、どういうときにMroonga・PGroongaを使うアプローチを選べばよいかという指針を示しました。

  • 全文検索の知識ナシ
    • 全文検索だけできれば十分
      • データが少ない(数十万件とか):RDBMSでLIKE
      • データが多い:Mroonga・PGroonga
    • イマドキの機能が必要
      • Mroonga・PGroonga
  • 全文検索の知識アリ
    • カリカリにチューニングしたい
      • RDBMS + 全文検索サーバー
    • それ以外
      • Mroonga・PGroonga

次に、以下の機能をMroonga・PGroongaで実現するにはどういうSQLを使えばよいか説明しました。

  • 全文検索
  • キーワードハイライト
  • 周辺テキスト表示
  • 入力補完
  • 同義語展開
  • 関連文書の表示

最後に、次のステップとして構造化データ(オフィス文書・HTML・PDFなど)の対応方法について少し言及しました。Groongaプロジェクトは構造化データからテキスト・メタデータ・スクリーンショットを取得するツールとしてChupaTextを開発しています。コマンドラインでもHTTP経由でもライブラリーとしても使えます。HTTP経由で使う場合はDocker・Vagrantを使うのが便利です。依存ライブラリーを揃える手間がないからです。

まとめ

Mroonga・PGroongaを使ってイマドキの全文検索システムを実装する方法を紹介しました。コンサルティングやチューニングや開発支援などを提供するサポートサービスがあります。社外秘のデータでお困りの場合はお問い合わせください。NDAを結んだ上で対応できます。

Mroongaはインサイト・テクノロジーさんが進めているPinkshiftでも活用されています。MySQL・MariaDB・PGroongaで高速全文検索が必要ならMroonga・PGroongaを試してみてください。

タグ: Groonga
2017-09-08

事前情報: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 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

«前月 最新
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|
タグ: