SyntaxHighlighter

2014年11月30日日曜日

Open TortoiseSVN for Google Chrome の Native Messaging Host 対応

Open TortoiseSVN for Google Chrome の Native Messaging Host 対応

前に書いたように、Google Chrome には、登録済みのローカルファイルを実行する仕組みがあり、Native Messaging と呼ばれています。
今回は、この具体的な作り方について書きます。

Native Messaging を使う Google Chrome 拡張機能

Chrome 拡張で Native Messaging を使うには、大きく分けて以下の対応が必要です。
なお、公式のページに、まとまった情報があります。

  • JSON 形式でやりとりする実行ファイルの準備
  • 実行ファイルのマニフェストファイルの準備
  • Chrome 拡張機能から、上記の実行ファイルの呼び出し

JSON 形式でやりとりする実行ファイル

やりとりするデータ形式

Native Messaging では、JavaScript 側と実行ファイル側が JSON 形式でデータをやりとりします。
JavaScript 側から渡すデータは JSON 形式にシリアライズされ、実行ファイルの標準入力から渡されます。
逆に、実行ファイル側からは、標準出力に JSON 形式でシリアライズしたデータを出力することで、JavaScript 側にデータを渡すことができます。

ただし、JSON データの長さ 4byte が、後にくる文字列の前に配置されます。

例えば、{"a":1234} というオブジェクトを、実行ファイル側から渡すことを考えます。
この JSON データの長さは 10 (0Ah) byte です。この場合、0A 00 00 00という 4byte が出力され、その後に JSON データが続きます。
サイズを示す先頭の 4byte は、Native Byte Order で渡すので、大抵の PC では Little Endian になります。

データ列の例
0A000000'{''"''a''"'':''1''2''3''4''}'

そのため、受け取る側はまず最初の 4byte を読み、そのサイズ分のデータを続けて読むことになります。

逆に実行ファイル側から出力する場合も同様です。

注意点

Windows 特有の注意点として、「標準入力と標準出力をバイナリモードで開く必要がある」ということがあります。

Windows では、ファイルを処理する際に「テキストモード」と「バイナリモード」があります。
説明は省きますが、標準入力や標準出力は「テキストモード」になっているので、その状態で \n を出力すると \r\n に自動で変換されることになります。
先の例では、0Ah\n なので、以下のようなバイト列になります。

データ列が変換される例
0D0A000000'{''"''a''"'':''1''2''3''4''}'

結果として、データを受け取る Chrome 側は 00000A0Dh つまり 2573byte の JSON データが送られてくると判断し、正しく動かなくなります。

そのため、C 言語であれば、以下のような Windows 依存のコードを実行しておくことで、標準入力や標準出力をバイナリモードにしておく必要があります。

#include <io.h>
#include <fcntl.h>

void set_binary_mode()
{
    _setmode(_fileno(stdin), _O_BINARY);
    _setmode(_fileno(stdout), _O_BINARY);
}

これらに注意して、好きな実行ファイルを作ります。

Open TortoiseSVN for Google Chrome の場合、 open_tortoise_svn_host.exe がその実行ファイルですが、この中では JSON を扱うために json11 を使わせてもらいました。
このライブラリは C++11 向けに書かれており、少し変更するだけで Visual Studio 2013 の C++ コンパイラでもコンパイルできます。

実行ファイルのマニフェストファイルの準備

作成した実行ファイルは、そのままでは Chrome から呼び出すことはできません。
呼び出せるようにするために、マニフェストファイルを作り、そのマニフェストファイルを Chrome に知らせる必要があります。

マニフェストファイル

公式のページにあるように、マニフェストファイルは以下のように定義します。

{
  "name": "com.my_company.my_application",
  "description": "My Application",
  "path": "chrome_native_messaging_host.exe",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
  ]
}

上記のようなマニフェストファイルを任意のファイル名で作成します。
name は、この Native Messaging 機能の名前を任意で決めて指定します。
path には実行ファイルの名前を指定します。実行ファイルは絶対パスで指定しますが、Windows ではマニフェストファイルがあるディレクトリからの相対パスでも構いません。
type は、現状 stdio しか指定できません。これは上で説明したような JSON 形式のデータを標準入力と標準出力でやりとりすることを意味しています。
allowed_origins には、この Native Messaging Host にアクセスしてよい拡張機能の名前を指定します。この名称は、拡張機能一覧のページ (chrome://extensions/) で ID をチェックすることで分かります。

というか、公式のページの説明を見ましょう。

マニフェストファイルの登録

続いて、作成したマニフェストファイルを Chrome に知らせる必要があります。

これも公式のページに書いてあるように、Windows の場合はレジストリで指定します。
HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_application にマニフェストファイルの絶対パスを書き込めばよいです。
HKEY_LOCAL_MACHINE 以下でもよいですが、HKEY_CURRENT_USER の下の方が管理者権限が不要なので楽でしょう。

Chrome 拡張機能から、上記の実行ファイルの呼び出し

Native Messaging には、実行ファイルが常駐するタイプと、毎回起動するタイプがあります。
Open TortoiseSVN for Google Chrome は後者のタイプです。

この場合、下記の公式ページの例のように sendNativeMessage を用いて、送信した JSON データと、受け取った JSON データを処理するためのコールバック関数を引数として渡すことで、好きな処理をすることができます。

chrome.runtime.sendNativeMessage('com.my_company.my_application',
  { text: "Hello" },
  function(response) {
    console.log("Received " + response);
  });

面倒になってきたので後半は省略しましたが、作成するのは簡単なのでローカルの実行ファイルと Chrome を連携させたい場合は利用するとよい機能だと思います。

Google Chrome の Native Messaging Host

Google Chrome の Native Messaging Host

Chrome 向けの Open TortoiseSVN を Native Messaging 対応したので、そのまとめです。
今回は、Native Messaging の概要についてです。

TechCrunch の記事にあるように、Chrome では NPAPI がブロックされます。
Open TortoiseSVN for Google Chrome では NPAPI を使っていたので、これを機に Native Messaging 版に変更しました。

Native Messaging

Native Messaging とは、 Google Chrome の Extension の機能の一つで、ローカルの実行ファイルを直接起動してやりとりするための仕組みです。
今回は、この機能を使ってローカルの TortoiseSVN を起動しています。

Native Messaging の概要

詳しくは Google Chrome の解説のページを見ると分かりますが、基本的には以下の通りです。

  • JavaScript から、あらかじめ登録済みのローカルの EXE を実行できる
  • その EXE と JSON 形式でメッセージのやりとりができる

なお、EXE と書きましたが、 BAT などでも問題ありません。

さらに、「EXE を実行してすぐに終了する」形式と「EXE を実行し、サービスのように常駐させてやりとりする」の二つの呼び出し形式がサポートされています。
また、JSON 形式でやりとりをする、という以外には、EXE の内容に制限はありません。

NPAPI プラグインとの違い

「ローカルの EXE (DLL) を呼び出し、JavaScript の制限外のことができる」という点では、NPAPI と似ています。
実際、これまで Open TortoiseSVN では、 NPAPI プラグインを使って、 JavaScript だけではできない「ローカルファイルを実行」を実現していました。

しかし、いくつか大きな違いがあります。

  • Native Messaging の場合、やりとりするデータは JSON に限られる
  • Native Messaging の場合、必ず別プロセスで起動される。

JavaScript のオブジェクトに比較的自由にアクセスできる NPAPI と異なり、Native Messaging では JSON 形式で明示的にやりとりしたデータしか EXE 側には渡らないので、意図せず JavaScript 側のグローバルオブジェクトにアクセスされたりせず、従来の JavaScript のセキュリティモデルは崩れません。
また、EXE を別プロセスで呼ぶため、クラッシュに悩まされることもなくなります。

PPAPI プラグインとの違い

一方で、NPAPI の置き換えとして Google が推進している PPAPI と比べると、PPAPI では呼べる API やローカルリソースへのアクセスに制限があるので、単純にできることが異なります。
基本的には PPAPI はサンドボックス化されており、NPAPI のように、任意の OS Native な API を呼べるわけではありません。

とても大雑把に言うと、NPAPI プラグインでできていたことのうち、「C/C++ で高速に動作するものを作りたい (が、ローカルリソースへの自由なアクセスは不要)」という部分は PPAPI に、「ローカルリソース (OS Native な API などを含む) に制限なくアクセスしたい」という部分が Native Messaging に分割され、整理されたということだと思います。

次回は Native Messaging を利用するための方法について、簡単に書きます。

2014年8月17日日曜日

ASCII 文字だけの場合も Windows BAT ファイルを LF 改行コードで保存してはいけない

ASCII 文字だけの場合も Windows BAT ファイルを LF 改行コードで保存してはいけない

とある作業中に気付いた BAT ファイルの謎挙動について、書いておきます。

概要

Windows では、通常テキストファイルの改行コードには CR LF の 2バイトを使います。
プレーンテキストで記述されている BAT ファイルも、当然のことながら CR LF を改行コードに使います。

しかし、実際には改行コードを LF にしても、問題なく動くことが多いです。
よく問題となるのは、日本語を含む BAT ファイルの場合で、改行コード LF で日本語を含むバッチファイルの動作がおかしい件改行コードがLFなバッチファイルなどで、実例が挙がっています。

しかし、最近、ASCII のみで構成された BAT ファイルであっても、改行コードが LF の場合に変な挙動を示すケースに出会いましたので、メモとして残しておきます。

結論としては、当たり前ですが、 CR LF で保存しましょう、ということ以外にないです。
最近は、テストの自動化などで、 Linux サーバーで動くソフトウェアから、 Windows 向けのテスト用 BAT ファイルを生成することもあるかと思いますが、注意しましょう。

再現方法

通常の動作

まずは、下記のような BAT ファイルを考えます。

この BAT ファイルを実行すると、 3行目の goto label で 10行目の :label までジャンプするので、 11行目で "good" が出力されて終了します。

これは、改行コードが LF であっても CR LF であっても、どちらでも同じ結果になります。

奇妙な動作

続いて、下記のような BAT ファイルを考えます。

先の例と比べ、 goto label でスキップされる部分に文字列を追加しただけなので、本質的な違いはありません。
そのため、実行すると "good" が出力されることが期待されます。

しかし、これを改行コードを LF として保存すると、 "bad" が出力されます。

BAT ファイルの謎の挙動

上述したことが起こるのは、 3行目の goto label と 14行目末尾の :label の間に 512 バイト (改行コード LF 含む) がある場合です。
より正確には、 512, 1024 など、512 の倍数が含まれている場合です。

この場合、どうやら 14行目末尾の :label をラベルと誤認するようです。

改行コードが LF の場合、これ以外にも 3行目の goto label と 19行目の :label の間が 512バイト (改行コード LF 含む) 付近の場合、 :label を見つけるのに失敗し、 goto label に失敗します。

そもそも BAT ファイルには意味不明な挙動が多いですが、うっかり改行コードを LF にすると、さらに謎の挙動に悩まされることになるので、 Linux サーバーなどで BAT を生成する場合は、注意しましょう。

追記

ちなみに、「とある作業」とは、 Windows 上での Ruby のビルドのことです。
Ruby をソースコードからコンパイルする際に、この現象に出会いました。

Revision 47015 から Revision 47204 までの間、 Subversion の EOL の扱いに起因して、ダウンロードのやり方によっては win32\configure.bat が LF になってしまっていました。
私が指定したコマンドラインオプションの場合、今回の現象が偶然に起こっていました。
現象を単純化するのにとても疲れました。

改行コードそのものは Revision 47205 で修正していただけました。

2014年6月22日日曜日

Rails で CSRF トークン検証エラーが出ることがある

Rails で CSRF トークン検証エラーが出ることがある

自分で作った Rails 製ウェブアプリで出会った話ですが、ちょっと原因に悩んだので書いておきます。

概要

Rails 製のウェブアプリのログを眺めていると、しばしば CSRF トークン検証エラーが記録されていました。ログインページでログインする際にデータを POST したときに、 CSRF トークン検証エラーが出たようです。
状況的に、外部から攻撃を受けているわけではなく、ユーザーもブラウザからアクセスしたようでした。

結局のところ、バグなどではなく、期待される挙動だったのですが、ちょっと悩んだので書いておきます。

再現手順

ブラウザ起動後の初回アクセスで、ウェブアプリのページを複数同時に開くと発生します。
「複数同時」というのがポイントです。

具体的には、以下の手順で発生します。

  1. 準備として Rails 製ウェブアプリで POST を使う複数のページを、二つブックマークに登録しておきます。
    例えば、「ログインページ」と「ユーザー登録ページ」などが該当するだろうと思います。
  2. 一旦ブラウザを閉じ、新たに開き直します。
  3. 「すべてのブックマークをタブで開く」などで、登録しておいた二つのページを同時に開きます。
  4. 開いた二つのページの少なくともどちらかでは、 POST 時に CSRF エラーが出ます。

原因や詳細

原因はセッション管理のされ方にあります。

Rails での CSRF 対応

Rails では、デフォルトで CSRF の対応がされています。
これは、セッション内に _csrf_token というキーで保存された値と、 POST 時に hidden field の authenticity_token で指定された値を比較することで、実現されています。
Ajax の場合には HTTP ヘッダ内に指定することもありますが、あまり関係ないので省略します。

要は、 CSRF トークンという、セッション毎にランダムに生成される値が、 POST で渡されてくる値と一致しているかを見ています。

セッションと Cookie

Rails では、セッションは URL Rewriting などではなく、デフォルトで Cookie を用いて実現されています。
また、 Cookie の有効期限はブラウザ終了までとなっています。

よって、デフォルトの設定では、セッションはブラウザを閉じたタイミングで破棄されます。つまり、ブラウザを開いた後の初回アクセス時に新規にセッションが生成されることになります。
この結果、 CSRF トークンは、ブラウザを開いた後の初回アクセス時に生成されます。
二回目以降のアクセスでは、セッション内に保存されている作成済みの CSRF トークンが使われます。

CSRF トークン検証エラーが起こる仕組み

以上のことから、複数のページを同時に開くと、以下の原理で CSRF トークン検証エラーが起こります。

  1. ブラウザを開きます。
  2. Rails 製ウェブアプリの複数のページを同時に開きます。
    仮に、ページ A, B とします。
  3. ページ A を GET する際に、新規にセッションが開かれ、それに伴い CSRF トークンが生成されます。
    このとき、ページ A 内の hidden field には、同じ CSRF トークンが設定されており、整合性がとれた状態になっています。
  4. これとほぼ同時に、ページ B が GET されます。
    ページ A の GET が完了してからであれば、ページ A で開かれたセッションが使われますが、ほぼ同時に開いた場合は、ページ A の GET は完了していないので、新規に別のセッションが開かれ、別の CSRF トークンが生成されます。
    ページ B 内の hidden field には、こちらの CSRF トークンが設定されることになります。
    この結果、ページ A の hidden field とページ B の hidden field には、別の CSRF トークンが authenticity_token として設定されることになります。
  5. この状態で、ページ A において POST をします。
    すると、セッションはページ B で開かれたものが使われます。
    これは、セッションが Cookie で管理されているためで、 Cookie は後から設定されたもので上書きされるためです。
  6. その結果、ページ A の hidden field の CSRF トークンと、セッション内の CSRF トークン (ページ B 由来のもの) は、異なるものとなり、 CSRF トークンの検証でエラーとなります。

最初は、どこかから CSRF 攻撃を受けているのかと思って、焦りました。
セキュリティ強度を下げずに回避するのは難しいかな。

2013年12月27日金曜日

SevenZipRuby 作成メモ 3 - マルチスレッド

SevenZipRuby 作成メモ 3 - マルチスレッド

7z ファイルを読み書きする Ruby gem ライブラリである seven_zip_ruby の作成メモその 3 です。
ひとまずこれでメモは終了し、久し振りに mruby の世界へ戻ろうと思います。

最後のメモはマルチスレッドや GVL (Global VM Lock) についてです。

概要

SevenZipRuby では、LZMA や BZIP2 圧縮の 7z ファイルを作る場合、デフォルトでマルチスレッドによる圧縮をサポートしています。
今回は、このマルチスレッドへの対応について、メモしておきます。

また rb_thread_call_without_gvl の使い方についても書いておきます。
ただし、(他の記事もそうですが) 今回の記事はあまり正しいかどうか、あまり自信がありません。

Ruby のマルチスレッド処理と GVL (Global VM Lock)

Ruby には Thread クラスがあり、手軽にマルチスレッドプログラミングを楽しむことができます。
ただし、MRI (CRuby) では複数の CPU Core がある環境で複数のスレッドを作成したとしても、同時に実行されるスレッドは高々一つになります。
これは GVL というグローバルな Mutex があり、これを取得しているスレッドしか動作しないように実装されているためです。

CPython でも同様な実装らしく GIL (Global Interpreter Lock) というものがあります。
詳しくは Wikipedia の GIL の項目などを見てください。

さて、GVL は、現状スレッドセーフに書かれていない CRuby の各関数や SevenZipRuby のような拡張機能が、マルチスレッド環境下で (そこそこに) 動くように守ってくれています。
GVL があることで、C の拡張機能を書いたときに、特定の Ruby の C API を呼んだときなど、こちらが明示的に指定した箇所でしかスレッドの切り替えが起こらないようになっています。
例えば、以下のような C 拡張機能のコードでは、スレッドの切り替えが起こりません。

この例の場合、Hoge#test の実行中は、"check" の出力が止まります。
これは、Ruby の C 拡張関数の実行中は GVL は取得されたままで解放されることがないため、他のスレッドが実行されたりしないためです。

ところで、VC++ 2010 などで試す場合、上の test 関数を以下のように定義すると、"check" が 0.1s ごとに出力されます。

これは、Ruby 側に細工があり、上記の例では Sleep 関数は Windows API ではなく、適切に GVL を解放してから待ちに入る rb_w32_Sleep が代わりに呼ばれるためです。
SleepEx であれば、このようなことは起こらないので、試す場合はこちらを使うとよいでしょう。

rb_thread_call_without_gvl による GVL の解放

先に挙げた SleepEx の例で、"check" が 0.1s ごとに出力されるようにするには、rb_thread_call_without_gvl を利用します。

この関数は、引数で指定した関数を、GVL を解放した状態で呼んでくれます。
そのため、上述した test 関数を以下のように書くと、"check" が 0.1s ごとに出力されるようになります。

このように書くことで、slow_func は GVL を解放した状態で実行されます。

ただし、注意点として、GVL を解放した状態では、ほぼすべての Ruby 関係の API を呼ぶことはできない というものがあります。
上記の slow_func の中では、例えば rb_gv_get でグローバル変数にアクセスしたりはできません。そのため、重い数値計算など、時間がかかる処理に限定して行うとよいでしょう。

ちなみに、Windows では Sleeprb_w32_Sleep になるので、上の例で SleepEx の代わりに Sleep を呼んでしまうと、GVL の扱いがおかしくなり、SEGV することになります。

7z.dll の呼び出し

以前に書いたように、7z.dll は内部で独自にスレッドを作成します。
そのため、以下のような方針で拡張機能を作ることにりました。

  • 7z.dll が作ったスレッド内で呼び出される Ruby のコールバック関数は、そこで直接処理しない
  • その処理をするために、別途 Ruby のスレッドを作り、イベントループのような動作をさせる
  • Ruby スレッドが 7z.dll のイベント待ちをする際は、条件変数で CPU を消費しないようにして待つ

イベントループの作成

概念的には、以下のようなループで 7z.dll のコールバック処理を行います。

この rubyEventLoop は Ruby のスレッドとして rb_thread_create から実行されます。
この中では、g_action_mutexg_action をガードしながら、g_action_cond_var の知らせを wait_for_event で待ちます。

イベント登録側は以下のようになります。

実際にはエラー処理などがありますが、概要としては以上のように Ruby 用のスレッドを用意して 7z.dll の処理を行っています。

イベント待ち

イベント待ちの部分では、適切に GVL を解放した上で Mutex の wait を呼ぶ必要があります。
上記の例の wait_for_event では、以下のように待つ必要があります。

このように GVL を解放して Mutex の wait を呼ばないと、wait を呼んだ途端に他の Ruby スレッドが GVL を取得することができなくなってしまうので、ハングします。

というわけで、マルチスレッドの場合は GVL をきちんと考慮して書く必要があります。

私自身もあまり深く分かっているわけではないですが、半年後ぐらいには seven_zip_ruby の実装を忘れてしまいそうなので、メモしておきました。

2013年12月15日日曜日

SevenZipRuby 作成メモ 2 - C++ での Ruby 拡張ライブラリのコーディング

SevenZipRuby 作成メモ 2 - C++ での Ruby 拡張ライブラリのコーディング

7z ファイルを読み書きする Ruby gem ライブラリである seven_zip_ruby も、無事に rubygems で公開しました。
ダウンロード数から察するに、あまり必要とされていないような気がしないでもないですが、以下にリンクをはっておきます。

今回は、 SevenZipRuby の作成メモ 2 として、 C++ での Ruby 拡張ライブラリのコーディングについて書いておきます。

概要

C++ での Ruby 拡張ライブラリのコーディングについては、 tueda の日記 - (Cではなく)C++でRubyの拡張ライブラリを作るには などが非常に分かりやすく書かれているので、とても参考になると思います。
また、 seven_zip_ruby では使いませんでしたが、 C++ で Ruby 拡張ライブラリを作成するための Rice というライブラリも公開されており、 Rice を使用して Ruby の拡張機能を C++ で作成するなどのページが参考になると思います。

今回は、私が個人的に気をつける必要があると感じた、例外まわりの実装などについて書いておこうと思います。

Ruby の例外機構

Ruby では、 raise "hoge"raise SyntaxError, "invalid syntax" のように例外を発生させ、それを外側のメソッドなどで begin, rescue, ensure を使って捕捉することができます。

この仕組みは、 MRI (CRuby) の場合、 setjmplongjmp によって実現されています。

setjmplongjmp

これらは C 言語で大域脱出を実現するための機構であり、例えば以下のようなことができます。

上記の例のように、 setjmp が呼ばれると、将来その関数の先で longjmp が呼ばれたときのために、「戻り先」を示すための CPU 依存の情報 (実行コンテキスト) を引数 gBuf に保存します。
その後、 longjmp が呼ばれると、引数で渡された実行コンテキストから、 setjmp を呼んだときの「戻り先」を求め、その状態に戻ります。

とてもとても大雑把に書くと、ネストされた関数内から外側の関数へ一度に移動できる goto のようなものです。
Java にはラベル付き break がありますが、「関数をまたいで使えるラベル付き break 」のようなイメージです。

より詳細な (そして正確な) 説明としては longjmpと例外などがとても分かりやすいです。

しかし、 goto などと異なり、 setjmp, longjmp は C++ で使う際には「デストラクタとの併用が困難」という大きな注意点があります。

C++ と setjmp, longjmp

例えば以下のようなコードを考えます。

この場合、 Constructor, Destructor, Error! と表示され、デストラクタ ~Test() が呼ばれていることが分かります。

一方で、以下のようなコードの場合、デストラクタは呼ばれません。

このように、 longjmp は強制的に setjmp を呼んだときのスタック状態に戻すので、 setjmp, longjmp の間で呼ばれるべきデストラクタは一切呼ばれません。
ここはとても重要なポイントです。

例外を考慮した C++ による Ruby の拡張ライブラリの作成

既に書いたように、 Ruby の例外機構は setjmp, longjmp を使って実現されています。
大雑把に書くと、 Ruby コード中の begin の部分で setjmp され、 raise Hoge の部分で longjmp されています。

そのため、 C++ で Ruby の拡張ライブラリを書く場合、前述したようなデストラクタの呼び出しがされない場合があることに注意が必要です。
例えば、 std::vector なんかを使っていると、簡単にメモリーリークが発生します。

C++ で書く場合は、こういったことを考慮して書く必要があります。
例えば、以下のように書く必要があります。

上記のように書けば、 Ruby の例外による longjmp の範囲を rb_protect で呼び出した sub_function の内部に閉じ込めることができます。

でも、それぞれの Ruby メソッドの呼び出しについて関数を分けるのは面倒です。

ラムダ関数の活用

なので、 C++ のラムダ関数を使ってみることにします。

例えば、以下のような関数を作っておいて、 rb_protect に渡すようにします。

こんな感じのテンプレート関数を作っておくと、あとはラムダ関数を使うことで、割と楽に Ruby の関数を呼び出すことができます。

C 言語と C++ の関数呼び出し規約におけるコンパイラ依存性

厳密には、上記の方法はコンパイラに依存しており、うまく動かないこともあります。

run_protect 関数の中で、 C 言語の関数である rb_protect を呼んでおり、その第一引数として run_functor_for_protect<T> のように C++ の関数ポインタを渡しています。

この部分がコンパイラに依存しており、うまく動かないかもしれない部分です。たぶん。

本来であれば、 C 言語の関数に関数ポインタを渡す場合は、 extern "C" 宣言された関数でなければなりません。

上の例のコードがうまく動くのは、「C 言語と C++ の関数の呼び出し規約が一致しているとき」です。
例えば「C 言語の関数の呼び出し規約は cdecl なのに、 C++ の関数の呼び出し規約は stdcall」みたいなコンパイラがあると、当然うまく動作しません。ただし、そんなコンパイラがあるのかは知りません。

C++ の仕様としては、こういう場合は extern "C" を付けることで、呼び出し規約 (と関数の名前修飾) が C 言語と同じになることが保証されていますが、テンプレート関数には extern "C" を付けることはできません。
名前修飾はともかくとして、呼び出し規約だけは C 言語と同じになるような書き方をしたいのですが、標準の書き方ではなさそうな感じです。

function クラスの利用

そのため、ラムダ関数とともに C++11 で追加された function クラスを使うことにします。

今回の場合では以下のようにすると、コンパイラ依存なコードにならないでしょう。

以上のように、 C++ で Ruby の拡張ライブラリを書くのは、割と面倒です。
seven_zip_ruby のように、呼び出し先の DLL が生の C++ を必要とするような環境でもない限り、 C で書くか、 Rice などのライブラリを使うのが楽でしょう。

2013/12/23 function に関する記述を追加

2013年11月5日火曜日

SevenZipRuby 作成メモ 1 - 7z.dll の概要と、 7z.dll と Ruby の橋渡し

SevenZipRuby 作成メモ 1 - 7z.dll の概要と、 7z.dll と Ruby の橋渡し

これから何回か、 SevenZipRuby を作成する際に悩んだことなどをメモしていこうと思います。

7z.dll のバージョンは 9.20 に基づいていますが、 7z.dll の作者の Igor さんによると、 9.30 でもこのインターフェースは使えるそうです。

なお、念のために断っておきますが、「7z.exe を呼んだらいいんじゃない?」というのはごもっともなのですが、 Ruby 拡張機能を作ること自体が目的なので、いろいろ面倒なことをしています。(DLL を呼ばないとできないこともありますし)


今回は、 7z.dll の概要と、 7z.dll と Ruby のバインディング部分を書くにあたって注意しなければならないことについて書いておきます。
結論としては、 7z.dll を呼ぶ以上、下記の二点を考慮しなければならず、面倒だ、という話です。

  • Ruby の例外機構 (setjmp, longjmp) を考慮した C++ のコーディング
  • 7z.dll が裏で生成する別スレッドを考慮した Ruby のメソッド呼び出し

7z.dll の仕様

今回の gem ライブラリでは、 7z.dll を内部的に呼ぼうと思ったので、 7z.dll の仕様を調べる必要がありました。

7z.dll の仕様は、断片的な情報しか見つかりませんでしたが、 7-Zip の FAQ の How can I add support for 7z archives to my application? に書かれているように、 7-Zip ソースコード中の Client7z.cpp を見るのが楽そうです。

以下では、私が SevenZipRuby を実装する際に調べたことをまとめておきます。
メモ書きだったものを、文体だけ変更して載せているので、あまり読みやすくないと思いますが、何かの参考になればと思います。

7zip アーカイブの展開の流れ

7zip アーカイブを 7z.dll を用いて展開する場合は、以下のような流れになります。

  1. 7z.dll から CreateObject 関数のポインタを取得する。
  2. CreateObject 関数で、 7zip アーカイブの展開用インターフェースである IInArchive インターフェースへのポインタを取得する。
  3. IInArchive でデータを読み込むために、下記のインターフェースの派生クラスを用意する。
    IInStream
    読み込み対象のファイルにアクセスするインターフェース
    IOutStream
    アーカイブ内のデータを展開する際の書き込み先のファイルにアクセスするインターフェース
    IArchiveOpenCallback
    IInArchiveOpen 関数を呼び出す際に必要なインターフェース
    IArchiveExtractCallback
    IInArchiveExtract 関数を呼び出す際に必要なインターフェース
  4. これらのインターフェースの派生クラスのインスタンスを作成し、 IInArchiveOpen, Extract 関数を呼び出す。

CreateObject

7z.dll では、種々のアーカイブをサポートしており、それらを扱うクラスは、 IInArchive もしくは IOutArchive クラスの派生クラスとしてそれぞれ定義されています。
7z.dll でアーカイブを扱う場合、そのアーカイブの種類に合った派生クラスのインスタンスを、 7z.dll がエクスポートしている CreateObject 関数を通じて取得する必要があります。そのため、まずは CreateObject 関数を DLL から取得する必要があります。
CreateObject 関数自体は、 DLL からエクスポートされているので、以下のように取得できます。

IInArchive インターフェースの取得

続いて、取得した CreateObject から、 7zip アーカイブを展開するためのインターフェースを取得します。

CreateObject 関数の第一引数には、 CLSID_CFormat7zCLSID_CFormatZip などのような、アーカイブのファイルフォーマットを示す GUID を渡します。
一覧は CPP/7zip/Guid.txt にまとまっているので、見るとよいでしょう。 例えば CLSID_CFormat7z であれば、 {23170F69-40C1-278A-1000-000110010000} になります。

第二引数には、 IID_IInArchiveIID_IOutArchive を指定します。
今回は、展開用のインターフェースが欲しいので、 IID_IInArchive を指定します。
こちらも、GUID の値は CPP/7zip/Guid.txt に載っています。 IID_IInArchive であれば、 {23170F69-40C1-278A-0000-000600600000} です。

あとは、これで得られた archive ポインタを通じて、好きな処理をしていくことになります。
なお、 IInArchive の定義は、 CPP/7zip/Archive/IArchive.hINTERFACE_IInArchive の部分を見ると分かります。 関数の名前から、だいたい意味は分かるのではないかと思います。

IInArchive でアーカイブを読み込むために必要な諸クラス

IInStream, IOutStream, IArchiveOpenCallback, IArchiveExtractCallback の派生クラスを、すべて定義しておく必要があります。

ここでは、サンプルとして IInStream の派生クラスの定義について記述します。

IInStream のメンバの定義

IInStream は、ファイルの読み込みを抽象化したインターフェースであり、以下の関数を持っています (Read は親クラスの ISequentialInStream のメンバーです) 。
なお、以下の記述は WINAPI などの呼び出し規約を書いていないので、そのままでは使えません。実際の定義では、 STDMETHOD マクロが使われています。

このインターフェースを継承したクラスを独自に作成することで、ファイルから読み込ませることや、ネットワークソケットから読み込ませることなどが自由にできます。

Ruby との橋渡し

7z.dll と Ruby のバインディングを行うには、 7z.dll が必要とする IInStream などのインターフェースを継承したクラスを作成し、そのクラスで適切に Ruby のメソッドを呼んでやることがメインとなります。

エラー処理などを省くと、イメージとしては下記のようになります。

このようにすることで、 7z.dll の世界と Ruby の世界を結ぶことができます。

しかし、上記のようなコードは期待した通りに動きません。
これは、大きくは以下の二点の理由によります。

  • Ruby の例外に対し、安全でない。
  • IInStreamRead, Seek が、別スレッドで呼ばれることがある。

Ruby (MRI) は C で実装されており、 Ruby の例外などの実装には setjmp, longjmp を使っています。
この関数によるジャンプは、 C++ のデストラクタ呼び出しを保証しないので、混在させて使うことができません。
例えば、 RubyInStream::Read の中で Ruby の例外が発生すると、 Read を呼び出した関数で定義されたローカル変数のデストラクタなどは、呼ばれないままにスタックの巻き戻しが発生します。これは容易に 7z.dll のクラッシュを発生させます。

二点目については、 7z.dll と Ruby の実装が深く関わっています。
7z.dll はマルチスレッドで動作するように設計されており、 Read は複数のスレッドから呼ばれることがあります。
7z.dll 側で同期をとった上で呼ばれているので、同時に複数のスレッドから Read が呼ばれることはないのですが、 Ruby の実装の制約上、 GVL という Mutex を取得していないスレッドが、 Ruby の関数を呼ぶことは禁止されています。
そのため、 7z.dll が生成した別スレッドから Read が呼ばれると、そのスレッドは GVL Mutex を取得していないので、 Ruby の関数を呼んではいけません。もし呼んでしまうと、 Ruby が Segmentation Fault で死ぬことになります。

というわけで、 7z.dll を Ruby から使えるようにするためには、以下の二点を考慮した設計にしなければなりません。

  • Ruby の例外機構 (setjmp, longjmp) を考慮した C++ のコーディング
  • 7z.dll が裏で生成する別スレッドを考慮した Ruby のメソッド呼び出し

なんて面倒なんだ。