SyntaxHighlighter

2011年12月30日金曜日

NPAPI プラグインの作り方 for Windows その4

NPAPI プラグインの作り方 for Windows その4

ずいぶん更新が止まってしまいましたが、NPAPI プラグインの作り方の続きです。

今回は、サンプルが動くようにしてみます。

なお、今回のサンプルは、GitHub のページからダウンロードできます。

サンプルの動作のさせ方

まず、前回作成したサンプルの NPAPI プラグインを動作させてみます。

ただし、もともとのサンプルのプラグインのバグが原因で、まだ正しく動作しません。

プラグインの登録

Firefox の場合

Firefox の場合は、インストールされたディレクトリ (C:\Program Files\Mozilla Firefox など) の下に plugins というディレクトリがあるので、その下に作成した DLL をコピーします。

これで、次回以降の起動でプラグインが動作するようになります。

Google Chrome の場合

Google Chrome の場合は、拡張機能を作成するのが簡単だと思います。

Google Chrome の拡張機能の作り方は、Chrome Extensions API リファレンスなどを参照してください。

バックグラウンドページのみを持つような拡張機能を作成し、以下のような plugins 属性を持った manifest ファイルを作成します。

1
2
3
4
5
6
"plugins": [
    {
        "path": "npsample.dll",
        "public": true
    }
],

publictrue にしておくと、プラグインをどのページからでも参照できるようになります。

ただし、これはセキュリティホールになることが多いので、テスト環境のみにした方が無難だと思います。

「ツール」-「拡張機能」を開き、デベロッパーモードにチェックを入れた上で「パッケージ化されていない拡張を読み込む...」で読み込むと、プラグインを使えるようになります。

テスト用 HTML の用意

前回の Firefox の公式ページのサンプル npruntime には、テスト用のファイル test.html が含まれていますので、これをブラウザで開きます。

画面の下の方にはいくつかボタンが並んでいると思いますが、これらを押すと、本来ならメッセージがポップアップ表示されるはずです。

しかし、ページ読み込み時かボタンクリック時に、ブラウザが「プラグインがクラッシュした」というようなメッセージを出してくると思います。

そうならない場合は、プラグインが正しく読み込まれていない可能性が高いので、手順を確認してください。

なお、test.html 内には二つの embed タグがありますが、今回作成したサンプルは、一つ目の embed タグに対して読み込まれます。

二つ目の embed タグに関しては「対応するプラグインが見つかりません。」のように表示されたままですが、今回は特に対応したりはしないことにします。

GitHub に用意しておいた test.html から、二つ目の embed タグは削除してあります。

サンプルの修正

ブラウザがクラッシュしないように修正します。

修正するのは plugin.cpp 内の3箇所です。

GibHub の diff が見易いのでこちらを参照した方がよいかもしれません。

452行目付近の _strdup
以下のように、*result_strdup の結果を代入するのではなく、NPN_MemAlloc でメモリを確保した上で *result に代入してやります。
1
2
3
4
5
-   STRINGZ_TO_NPVARIANT(_strdup("foo return val"), *result);
+   const NPUTF8 *ret_str = "foo return val";
+   NPUTF8 *s = (NPUTF8 *)NPN_MemAlloc(strlen(ret_str) + 1);
+   strcpy(s, ret_str);
+   STRINGZ_TO_NPVARIANT(s, *result);
466行目付近
こちらも、まったく同様に _strdup によるコピーを修正します。
540行目付近
以下のように、NPN_Evaluate の最後の引数に変数を渡してやり、戻り値を受け取れるようにします。
この値は不要なので、忘れず NPN_ReleaseVariantValue で解放してやります。
1
2
3
4
-   NPN_Evaluate(m_pNPInstance, doc, &str, NULL);
+   NPVariant rval;
+   NPN_Evaluate(m_pNPInstance, doc, &str, &rval);
+   NPN_ReleaseVariantValue(&rval);

この状態でコンパイルしなおし、DLL をコピーした上で、もう一度サンプルの test.html を閲覧すると、クラッシュせずに動作することが確認できると思います。

次回は、このサンプルにディスクサイズを取得するメソッドを追加し、もう少しサンプルの動作を説明する予定です。

2011年12月12日月曜日

Redmine Plugin で ApplicationHelper を拡張する際の注意

Redmine Plugin で ApplicationHelper を拡張する際の注意

Windows PC の調子が悪いので、NPAPI プラグインの作り方は少し延期です。

今回は、Redmine 1.3.0もリリースされたので、 Redmine Plugin で ApplicationHelper を拡張する際に悩んだ問題について書きます。

Redmine プラグインで、既存のクラスやモジュールを拡張する

Redmine プラグインを作成し、 ApplicationHelper などの既存のクラスやモジュールを拡張する場合、基本的には以下のようにします。

  1. プラグインのlib以下にredmine_sample_plugin_application_helper_patch.rbのようなファイルを作成し、以下のように記述します。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    require_dependency("application_helper")
     
    module SamplePluginApplicationHelperPatch
      def self.included(base)
        base.send(:include, InstanceMethod)
     
        base.class_eval do
          # 既存のメソッドを置き換えたりする場合は、
          # ここで alias_method などを使う。
        end
      end
     
      module InstanceMethod
        # ApplicationHelper に新規追加するメソッドを記述する。
        def sample_instance_method
          # 何らかの処理
        end
      end
    end
     
    # ApplicationHelper モジュールに SamplePluginApplicationHelperPatch を
    # インクルードさせる。
    ApplicationHelper.send(:include, SamplePluginApplicationHelperPatch)
  2. init.rb内部で、redmine_sample_plugin_application_helper_patch.rbrequireします。

上に書いたように、一般的にはApplicationHelperを再定義せずに、追加したり置換したりしたいメソッドを別モジュールにまとめた上で、これをインクルードさせることが多いです。

View のフック関数からの呼び出し

Redmine のプラグインへのフレームワークとして、View の決められた場所に、プラグインのコードを埋め込む、という仕組みがあります。

ここでは説明しませんが、知りたい方はRedmine自体に手を入れずに見た目を変更する方法などをご覧ください。

いずれにせよ、Redmine::Hook::ViewListenerを継承したクラスを作成し、その中にあらかじめ定められた名前のフックメソッドを定義することで、View に任意のコードを追加できます。

View のフック関数から、追加したメソッドの呼び出し

上のSamplePluginApplicationHelperPatchのように定義した場合、新規に定義したメソッド (上の例ではsample_instance_method) は、View のフック関数から呼ぶことができません

ApplicationHelper 内のメソッドは、すべてのコントローラーや View から呼べるはずなのですが、呼ぶことができないのです。

呼び出せない理由

ApplicationHelperは、Redmine::Hook::ViewListener内で適切にincludeされています。
また、上で定義したサンプルでは、確かにSamplePluginApplicationHelperPatch::InstanceMethodApplicationHelperにインクルードされています。
そのため、間接的にSamplePluginApplicationHelperPatch::InstanceMethodRedmine::Hook::ViewListenerからインクルードされているように思えます。

しかし、実際にはそうなりません。

Redmine::Hook::ViewListenerApplicationHelperをインクルードするのは、プラグインの各ファイルがロードされるよりも早いタイミングで行われます。
そのため、Redmine::Hook::ViewListenerApplicationHelperをインクルードする頃には、まだSamplePluginApplicationHelperPatch::InstanceMethodApplicationHelperにインクルードされていません。

Ruby では、includeしたタイミングで継承関係が決定します。

例を挙げると、以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module A
end
 
module B
  include(A)
end
 
module C
end
 
p B.ancestors
# => [ B, A ]
 
A.send(:include, C# A.ancestors に C を追加
p B.ancestors
# => [ B, A ]     # C は含まれません。
 
module D
  include(A)
end
p D.ancestors
# => [ D, A, C ]  # C が含まれます。

上の例で、B.ancestorsCが含まれてもよさそうですが、含まれません。

これは奇妙なように思いますが、 Ruby の仕様であり、どうしようもありません。

余談ですが、 Ruby 作者のまつもとさんも、これは奇妙に思っているらしく、Matzにっきの Traits の項目に、今後mixを追加しようと思っている旨が書かれています。

そのため、もともとの Redmine の ViewListener に関しても同様に、Redmine::Hook::ViewListenerApplicationHelperをインクルードするタイミングでは、まだSamplePluginApplicationHelperPatch::InstanceMethodApplicationHelperにインクルードされていないので、結果としてRedmine::Hook::ViewListenerの ancestors にはSamplePluginApplicationHelperPatch::InstanceMethodが含まれないことになります。

回避策

これは、例えば以下のようにすれば回避することができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require_dependency("application_helper")
 
module SamplePluginApplicationHelperPatch
  def self.included(base)
    base.send(:include, InstanceMethod)
 
    base.class_eval do
      ###########################
      # alias_method で ApplicationHelper にメソッドを追加
      alias_method :sample_instance_method, :def_sample_instance_method
    end
  end
 
  module InstanceMethod
    # ApplicationHelper に新規追加するメソッドを記述する。
    def def_sample_instance_method
      # 何らかの処理
    end
  end
end
 
# ApplicationHelper モジュールに SamplePluginApplicationHelperPatch を
# インクルードさせる。
ApplicationHelper.send(:include, SamplePluginApplicationHelperPatch)

このようにすることで、sample_instance_method自体はApplicationHelperに直接追加されるので、Redmine::Hook::ViewListenerからでも呼び出すことが可能になります。

もちろん、base.class_eval doの内部で、直接sample_instance_methodを定義しても呼び出すことができますが、このあたりはコードの見易さの問題で、好みの分かれるところだろうと思います。