» アドオン開発

Mozilla 勉強会@北海道の資料を公開しています

先週末、OSC 北海道の中で Mozilla 勉強会@北海道 を開催しました。暑い中ご参加いただき、ありがとうございました。スライドやデモなどの資料は上のリンクからご覧いただけます。

私が担当したセッション「イチから始める Firefox 拡張機能開発」の資料は ZIP ファイル でダウンロードできます。プレゼン (PDF) には関連リンクも含まれていますので、適宜参照してください。また modest 内の 拡張機能開発入門 にも各種ドキュメントへのリンクが載っています。

当日デモに使った「天気はどう?」拡張機能は tenkiwado.xpi というファイルです。Firefox のウィンドウ内にドラッグ&ドロップすれば直接インストールできますし、これは実際には ZIP ファイルなので、拡張子を変えれば展開してソースコードを見ることができます。JavaScript には詳しいコメントも入れていますので、実際にどういう仕組みで動いているか、参考にしてください。

実はもうひとつ「ニュースはどう?」という拡張機能 newswado.xpi もセッションの直前に作ってありました。これは北海道新聞の速報ニュースをティッカー表示するツールバーです。ソースコードは簡単ですので、同じように展開して見てみてください。

拡張初学者用スケルトン

 AMOアドオンビルダーの作成するスケルトンがあまりに酷いのでごくシンプルなスケルトンを作成しました。
 大元はAMOアドオンビルダが作成するスケルトンですが以下のものを省きました。

  • ローカライズの処理
  • 呼び出されることのないイベント・ハンドラ
  • アクセス不能なオブジェクト
  • 初学者には敷居の高い XPCOM コンポーネントの呼び出し

 また、若干ですが Firefox にオーバーレイするXULファイル、ならびにファイル構成も簡素化しています。ただし、このスケルトンでは使っていない local、skin のディレクトリはそのまま残しています。

 再利用される場合は、他の拡張とコンフリクトを起こさないように修正してください。

/wp-content/uploads/2010/06/skeleton.zip

 以上の説明で用語や概念等について解からないこともあるかと思いますので、その場合はFirefox拡張機能開発チュートリアルを参照してください。

JSMをimportしているコード内のfunctionをJSM側から呼び出すには

みなさんは JavaScript Code Module ( 以下 JSM と略記します ) を書いていて、その JSM を import するコード側の function を JSM 側からコールしたい、といったニーズが発生したことはないでしょうか。
つまり以下のようなことをしたいケースです。

// hoge.jsm 側のコード
function hoge(){
		...
	changeXUL(); // JSM を import する側で定義されている function
		...
};
//JSM を import する側のコード
{
	Components.utils.import("resource://hoge/hoge.jsm",scopeHoge);
	let changeXUL=function(){ // JSM 側から呼び出したい function
		...
		// ブラウザの要素を DOM で変更する処理など
		...
	};
		...
}

上記コードはこのままでは動きません。hoge() 内でコールされている changeXUL() が undefined となります。
しかし hoge() から changeXUL() をコールしたいケースがないわけではありません。ここでは hoge() から changeXUL() をコールする方法を解説します。


上記の方法では hoge() から changeXUL() をコールできないことは先に述べました。hoge() から changeXUL() を呼び出すには、changeXUL() への参照を JSM 側に持つことで解決します。

最初に JSM 側に changeXUL() への参照を格納する変数を定義します。

var funcCallback;

同じく JSM 側に変数 funcCallback に changeXUL() への参照を設定する setter を定義します。

var EXPORTED_SYMBOLS=[
	"setCallback"
];
		...
function setCallback(pfunc){
	funcCallback=pfunc;
}

今度は JSM を import するコード側であらかじめ先の setter をコールするコードを書いておきます。

{
	let scopeHoge={};
	Components.utils.import("resource://hoge/hoge.jsm",scopeHoge);
		...
	let changeXUL=function(){ // JSM 側から呼び出したい function
		...
		// ブラウザの要素を DOM で変更する処理など
		...
	};
		...
	let init(){
		scopeHoge.setCallback(changeXUL); // setter のコール
	};
}

更に本来は hoge() 内に書きたかった changeXUL() の代りに changeXUL() への参照 funcCallback を書いてやります。

// hoge.jsm 側のコード
function hoge(){
		...
	funcCallback(); // changeXUL() への参照の呼び出し
		...
}

後は JSM 側で hoge() がコールされれば、changeXUL() への参照である funcCallback を通して changeXUL() のコードが実行されます。

以上のコードを整理して記述すると以下のようになります。

// hoge.jsm
var EXPORTED_SYMBOLS=[
	"setCallback"
];
var funcCallback;
		...
function setCallback(pfunc){
	funcCallback=pfunc;
}
		...
function hoge(){
		...
	funcCallback(); // changeXUL() への参照の呼び出し
		...
}
// hoge.jsm を import する側
{
	let scopeHoge={};
	Components.utils.import("resource://hoge/hoge.jsm",scopeHoge);
		...
	let changeXUL=function(){ // JSM 側から呼び出したい function
		...
		// ブラウザの要素を DOM で変更する処理など
		...
	};
		...
	let init(){
		scopeHoge.setCallback(changeXUL); // setter のコール
	};
}

以上の方法のなにが嬉しいのかといいますと、私の場合であれば JSM 内で XMLHttpRequest を非同期で利用する際、コールバック内でブラウザの要素を書き換えたい場合等に利用しています。

フォク蔵を ‘How to Improve Extension Startup Performance‘ に対応させるのに、この手法を利用しています。MPL、GPL、LGPL のトリプルライセンスでソースを公開していますので、自由に参照してください。


【追記】
dynamis さんが、よりシンプルな方法をコメントしてくださいました。ありがとうございます。
以下は dynamis さんが紹介されている “hoge(… , callback)” のように、コールバック関数を直接 JSM 内の function に引数として渡す方法のサンプルです。

// hoge.jsm
var EXPORTED_SYMBOLS=[
	"hoge"
];

function hoge(pstr,funcCallback){
		...
	funcCallback(pstr); // hoge.jsm を import している側のコードの dispStr() を実行
	                    // 'Hello Hogeヽ(°▽°、)ノ'が表示される
		...
}
{
	// JSM を import する側のコード
	let scopeHoge={};
	Components.utils.import("resource://hoge/hoge.jsm",scopeHoge);
		...
	let dispStr=function(pStr){
		alert(pStr);
	};
		...
	let sampleOrg=function(){
		...
		scopeHoge.hoge('Hello Hogeヽ(°▽°、)ノ', dispStr);
	};
		...
}

原理は最初のものと一緒ですが、ぐっとシンプルになって、コードの見通しも良くなっています。

ちなみに dynamis さんの方法と私の方法の違いは、私の方法は hoge() がJSM を import している側のコードから、ネストも含めて直接呼ばれないことを前提としている点です。
目的に合わせてそれぞれ使い分けるといいかと思います。

JavaScript コードモジュール

拡張機能開発時でよく使う再利用性のあるコードをインポート可能な形で書いたものを JavaScript コードモジュールといいます。コードモジュールは単に再利用できるだけでなく、何度読み込んでも最初の一度だけ初期化されて使い回されるシングルトンになるという特徴があるため、高速に動作する拡張機能開発のベストプラクティスとしても知られています。

コードモジュールは Firefox 3 からサポートされており、これから新規に拡張機能を開発するにあたっては遠慮無く使っていける時期になってきているはずです。みんなでコードモジュールを使って、作って、共有していきたいですね。(・・).

ここではいくつかのコードモジュールを紹介します。他にも公開されているコードモジュールがあれば皆さん随時追記していってください。

Firefox 標準搭載のコードモジュール

Mozilla Labs で開発されているコードモジュール

詳細は https://wiki.mozilla.org/Labs/JS_Modules を参照 。

  • JSON
  • Logging
  • Observers
  • Preferences
    • 環境設定 (prefs) の操作に便利です。型を気にせず一括設定や読み込みできます
  • StringBundle
    • l10n の properties ファイル読み込みが楽になります。xul に bundle 要素書かずに済みますし
  • Sync
  • URI

Mozilla Labs 製の一部のモジュールについては、Firefox Sync または Firefox 4 本体に同梱されています。上記のページにリンクされているものよりも新しいバージョンのモジュールもあるので、 http://hg.mozilla.org/mozilla-central/file/tip/services/sync/modules/ext/ を参照するのもよいかと。

その他のコードモジュール

  • jstimer.jsm – written by Piro さん
    • nsITimer を wrap して使い慣れている setTimeout / setInterval のように使えるようにしたモジュール
  • jquery.js – written by dynamis based on jQuery
    • jQuery を拡張機能の互換性問題など生じずに使えるようにした「つもり」のモジュール
    • animation, mouse event, ajax 周りは使えることを確認済みだが他は未検証
    • jQuery は HTML であり XUL 要素に対して同じ効果を実現できないこともあるハズ
  • console.js – written by dynamis
    • Firebug と連携して Firebug 感覚でデバッグするためのモジュール
    • loglevel の設定、Firebug のコンソールへの出力、dump 出力など
  • namespace.jsm – written by Piro
    • 複数のアドオンにまたがった共通の名前空間を作成するためのモジュール。
  • animationManager.js – written by Piro
    • JavaScriptベースのアニメーション効果を実装する際に、各アドオンで別々にタイマーを走らせずに単一のタイマーで処理を行うためのモジュール。アニメーション効果が軽くなることが期待できる。JavaScriptコードモジュールとして使用する場合はjstimer.jsmに依存し、namespace.jsmとの併用に対応。
  • autoScroll.js – written by Piro
    • ドラッグ操作中におけるタブバーの自動スクロールを実現するためのモジュール。namespace.jsmとの併用に対応。
  • boxObject.js – written by Piro
    • nsIBoxObjectと共通のインターフェースで任意のDOM要素の位置と大きさを取得するモジュール。Geckoから削除された機能であるHTMLDocument.getBoxObjectForを使っていたコードを、最小限の変更で最近のGeckoに対応させるための物。namespace.jsmとの併用に対応。
  • prefs.js – written by Piro
  • stringBundle.js – written by Piro
  • CLHHelper.jsm – written by クリアコード
    • nsICommandLineHandlerインターフェースを備えたXPCOMコンポーネントの開発を支援するモジュール。
  • ejs.jsm – written by クリアコード
  • encoding.jsm – written by クリアコード
    • UTF-8、Shift_JIS、EUC-JPなど、文字列のエンコーディングを変換する処理を簡単に書くためのモジュール。
  • hash.jsm – written by クリアコード
    • 文字列やファイルのハッシュの計算結果を簡単に取得するためのモジュール。
  • action.jsm – written by クリアコード
    • ボタンのクリックやキーの押下など、ユーザの操作をDOMイベントのレベルでエミュレートするためのモジュール。

その他いろいろな拡張機能に独自のコードモジュールが含まれているので色々探してみてください(いいものがあれば紹介を追加してください)。

JavaScript ライブラリ

JavaScript コードモジュール形式になっていないが、便利に使い回せる JavaScript ライブラリです。

  • Piro さんのライブラリ集
    • 拡張機能開発者といえば Piro さん。(^^;
    • 独立性の高いコードになっているので prefs.js を元に冒頭に EXPORTED_SYMBOLS と window だけ定義してコードモジュール化した prefs.js にできるなど、簡単にコードモジュール形式にできるものが多いそうです!→という風な改造を特に施さなくても、コードモジュールとして読み込んでも<script/>で読み込んでも使えるようにしてみました。(by Piro)

references

JavaScript コードモジュールについて詳しい解説は以下のページをご覧ください。

XPCOM サービスへの頻繁なアクセスを効率化するテクニック

拡張機能や XUL アプリにて、 JavaScript から特定の XPCOM サービスを頻繁に参照する際に、いったん取得した参照をキャッシュすることで効率化することがよくあります。そのためには、取得した参照を単にグローバル変数として保持する方式や、初回参照時にオブジェクトのゲッタとして定義する方式など、色々な方式があります。また、 Firefox 3.6 (Gecko 1.9.2) ではキャッシュを行うための汎用の XPCOMUtils.defineLazyServiceGetter が導入されたことで、新しい書き方ができるようになりました。それらの方式をまとめたブログ記事を書きましたので、ご覧ください。

SCRAPBLOG : XPCOM サービスへの頻繁なアクセスを効率化するテクニック

xul:textbox のプレースホルダー文字列

注意: この記事は Firefox 3.7 での新機能について触れています。

HTML のテキストボックス

Firefox 3.7a5pre では HTML 5 の仕様である placeholder 属性 が実装されており、以下のようにしてテキストボックスにヒント用文字列を表示させることができます。

<input type="text" placeholder="Your Name">

XUL のテキストボックス

一方、 XUL の textbox 要素では Firefox 3.0 にてすでに同様の機能が emptytext 属性として実装済みですが、 Firefox 3.7a5pre では placeholder 属性でもヒント用文字列を表示することができます。互換性維持のため emptytext 属性も引き続き利用可能ですので、 Firefox 3.6 と 3.7 両対応の拡張機能などでは emptytext 属性を使用したほうが良いでしょう。

<textbox emptytext="Your Name" />
<textbox placeholder="Your Name" />

また、以下のように xul:textbox 要素の emptyText (大文字小文字に注意)および placeholder プロパティを使って JavaScript で動的にヒント用文字列をセットすることも可能です。

document.getElementById(...).emptyText = "Your Name";
document.getElementById(...).placeholder = "Your Name";

nsIEventListenerService でDOMイベントリスナを列挙する

nsIEventListenerService というXPCOMサービスを使うと、 XUL や HTML ドキュメント内のある要素に対してどんなDOMイベントリスナが追加されているかを調べることができます。以下は、 browser.xul にてFirefoxの「ホーム」ボタンに追加されたイベントリスナをエラーコンソールに列挙するサンプルです。

var els = Cc[&quot;@mozilla.org/eventlistenerservice;1&quot;].
          getService(Ci.nsIEventListenerService);
var infos = els.getListenerInfoFor(document.getElementById(&quot;home-button&quot;), {});
infos.forEach(function(info) {
	Application.console.log(info.type + &quot; =&gt; &quot; + info.toSource());
});

nsIEventListenerService の getListenerInfoFor メソッドは、引数で渡した要素のイベントリスナの情報を、 nsIEventListenerInfo オブジェクトの配列として返します。さらに、各 nsIEventListenerInfo オブジェクトについて、 type プロパティでイベントリスナの種類(click, keypress, mousedown など)を調べたり、 JavaScript のリスナであれば toSource() で内容を文字列化したりできます。ただし、 nsIEventListenerInfo オブジェクトはイベントリスナそのものではないので、 getListenerInfoFor で取得したイベントリスナを removeEventListener で削除する、といったことはできません。あくまでもデバッグ用です。

なお、 nsIEventListenerService は Firefox 3.6 (Gecko 1.9.2) 以降で利用可能です。

一定時間ドラッグオーバーし続けたら処理を実行する

拡張機能(XULアプリ)にて、一定時間ドラッグオーバーし続けたときに何らかの処理を実行したい、例えばツールバーに配置したボタン上にブラウザタブを3秒間ドラッグオーバーし続けたら、そのボタンをクリックしたものとみなして処理を実行したいとします。

これは、HTML5のドラッグ&ドロップAPIを使い、ドラッグオーバーし続けた際に dragover イベントが繰り返し発生する特性を利用すると、以下のように実装可能です。

以下は、ボタン上に何かを3秒間ドラッグオーバーし続けると、テキストボックスに現在時刻を表示するサンプルです。なお、サンプルコード全量はこちらに置いてあります。 chrome 権限は不要ですので、ダウンロードして拡張子を.xulにしてFirefoxで開けば、動作確認可能です。

XUL:

<button label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragover="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '\n';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {

    _dragStartTime: null,

    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter": 
                // ドラッグオーバー開始時、ドラッグオーバー開始時刻をセット
                this._dragStartTime = Date.now();
                break;
            case "dragover": 
                // ドラッグオーバー中、ドラッグオーバー開始時刻からの経過時間を調べる
                if (this._dragStartTime && Date.now() - this._dragStartTime > 3000) {
                    // 3秒以上経過したら、ドラッグ開始時刻をリセットし、処理を実行する
                    this._dragStartTime = null;
                    event.target.doCommand();
                }
                break;
        }
    }

};

タイマーを用いた実装方式

ドラッグオーバー開始時(dragenter イベント発生時)に setTimeout で一定時間後に処理を実行するためのタイマーを設定し、ドラッグオーバー終了時(dragleave イベント発生時)に clearTimeout でタイマーを解除する、という実装方式ももちろん可能です。

XUL:

<button id="myButton"
        label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragleave="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '\n';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {
    _dragOverTimer: null,
    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter": 
                // dragenterイベントが二回連続で発生した場合への対策
                if (this._dragOverTimer)
                    return;
                // ドラッグオーバー開始時にタイマーを設定
                this._dragOverTimer = setTimeout(function() {
                    document.getElementById("myButton").doCommand();
                }, 3000);
                break;
            case "dragleave": 
                // ドラッグオーバー終了時にタイマーを解除
                clearTimeout(this._dragOverTimer);
                this._dragOverTimer = null;
                break;
        }
    }
};

ペルソナの技術詳解

ペルソナを使用している方は結構いるかと思います。
今日はそのペルソナの技術的な話をしたいと思います。

ペルソナ設定データ

ペルソナの設定データはabout:configなどから見られる、lightweightThemes.usedThemesが主な設定データとなっています。これは以下のようなプロパティを持つJSON形式の配列となっています。

[
  {
    id: "ペルソナのID番号(必須)",
    name: "テーマの名前(必須)",
    author: "作者の名前",
    accentcolor: "背景色",
    textcolor: "テキスト色",
    headerURL: "ツールバーの背景画像URL(必須)",
    footerURL: "ステータスバーの背景画像URL",
    previewURL: "アドオンの管理画面から見られるテーマの画像URL",
    iconURL: "アドンの管理画面から見られるテーマのアイコン画像URL",
    updateURL: "JSON形式でアップデート情報を出力するURL"
  },
  {
    //...
  }
]

強調部分のプロパティは必須です。また、~URLhttpから始まる文字列である必要があります(そうでないと通常の方法では反映されない)。

ペルソナの有効化

ペルソナを有効にすると以下のようなことが起こります。

  • window要素の lwtheme 属性が true になります
  • window要素の lwthemetextcolor 属性が bright または dark になります
    • textcolor が影響しており、このRGB値から輝度を計算し、明るいなら bright、暗いなら darkとなります
    • また、bright なら :root:-moz-lwtheme-brighttext というCSSセレクタが有効になり、ツールバーなどのラベルに黒のシャドウが付きます。dark なら :root:-moz-lwtheme-darktext セレクタが有効になり、白のシャドウが付きます
  • window要素の style 属性が設定されます
    • colorプロパティにtextcolor(値がない場合は”black”)
    • background-colorプロパティにaccentcolor(値がない場合は”white”)
    • background-imageプロパティにheaderURL
  • window要素の drawintitlebar 属性に true が設定される(MacOS Xのみ)
  • statusbar要素の style 属性が設定されます
    • colorプロパティにtextcolor(値がない場合は”black”)
    • background-colorプロパティにaccentcolor(値がない場合は”white”)
    • background-imageプロパティにfooterURL
  • 各要素のCSSの以下の擬似要素が有効になる
    • :-moz-lwtheme
    • :-moz-lwtheme-brighttextまたは:-moz-lwtheme-darktext
      (window要素の lwthemetextcolor 属性値に左右される)

また、同時にheaderURLとfooterURLの画像がプロファイルディレクトリ下の lightweighttheme-headerlightweightthtme-footer にダウンロードされます。これは、ペルソナが有効な状態でFirefoxを再起動したときに使われる画像となります。

JavaScriptモジュール

ペルソナで使用するJavaScriptモジュールには以下の2つがあります。

  1. LightweightThemeConsumer.jsm
  2. LightweightThemeManager.jsm

これらは Firefoxインストールディレクトリ/modules/ ディレクトリ内にありますので、ソースを読みたい方はどうぞ。

LightweightThemeManager.jsm

“Manager”と付いている通り管理を担当するモジュールです。

  • ペルソナデータの取得
  • ペルソナデータの削除
  • ペルソナデータの追加と変更通知
Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");

などとインポートして使用します。

データ取得

var themes = LightweightThemeManager.usedThemes

usedThemes プロパティはペルソナ設定データで挙げた lightweightThemes.usedThemes を取得して JSON.parse した値を返すゲッタとして定義されています。返ってくる値はオブジェクトの配列です。

データのIDがわかっている場合は

var theme = LightweightThemeManager.getUsedTheme(aId);

からも取得可能です。

現在使われているデータは

var currentTheme = LightweightThemeManager.currentTheme

var currentTheme = LightweightThemeManager.currentThemeForDisplay

から取得可能です。
currentThemeForDisplay の方は、headerURL,footerURL に限り、ローカルファイルへのパスに変換されたデータとなります。

データ削除

LightweightThemeManager.forgetUsedTheme(aId);

データ追加と変更通知

LightweightThemeManager.currentTheme = aThemeData;

aThemeDataペルソナ設定データのプロパティを持つ必要があります。
データの取得にも出てきた currentTheme を使用します。
これはセッタとして定義されていて、データの追加や順番の入れ替えてPrefernecesへの反映は行いますが、実際の画像などの反映は行わずに通知を行います(理由は後述)。

LightweightThemeConsumer.jsm

実は LightweightThemeManager.jsm 側では実際のデータから画像やテキスト色の変更は行っておらず、こちらのモジュールが受け持っています。

何故なら、”Manager”側でそれをやろうとすると、Firefoxのウィンドウが複数あった場合にそれぞれに変更をかけるのは面倒だからです。
“Consumer”モジュールは意識せずとも内部で自動的にインポートされており、各ウィンドウで動作するようになってして、変更通知と共にデータを受け取って実質的な変更を行う仕組みとなってします。

LightweightThemeManager.jsmはドキュメントルートにXBLでバインドされるmozilla-central mozilla/toolkit/content/widgets/general.xmlで使用されています。よってこのモジュールは新たにインポートする必要はありません。

そして、lightweight-theme-changedというTopic名の通知を監視して、この通知と共にDataにくる値を画像やテキスト色などペルソナの有効化で書いた内容の変更がなされます。

Webサイトからの配布の仕組み

http://www.getpersonas.com/から実際にプレビューやインストールできますが、その仕組を解説しましょう。

ペルソナは DOM Event を発行することでプレビューやインストールがされます。
DOM Eventには以下の3つのイベントがあります。

  • InstallBrowserTheme
  • PreviewBrowserTheme
  • ResetBrowserThemePreview

これらのイベントを特定の属性を持った要素から dispatchEvent してあげます。

特定の属性とは、data-browsertheme という属性名で、属性値にはペルソナ設定データのJSONコードが入っている属性です。以下のような感じです。

<img src="preview.png"
     data-browsertheme='{"id":"1000","name":"sample","headerURL":"http://example.com/header.png"}' />

実際にはHTMLに直接埋め込むことはせず、JavaScriptで属性を付加することになるでしょう。

var theme = {
  id: "1000",
  name: "sample",
  headerURL: "http://example.com/header.png"
};
var themeElem;
function init(){
  themeElem = document.getElementById("theme");
  themeElem.setAttribute("data-browsertheme", JSON.stringify(theme));
}
window.addEventListener("load", init, false);

コード全容は、Lightweight themes – MDCにコードのサンプルがありますので、ご参考に。
私自身もhttp://www.paw.hi-ho.ne.jp/makochi/personas/に実験的に作ってみたサンプルがあります(これはzenper :: Add-ons for Firefoxなどのアドオンを入れている場合用です)。

また、Firefox側でのイベントリスナのコードはmozilla-central identifier search “LightWeightThemeWebInstaller”を見ると良いでしょう。

Firefox 3.7でのjs-ctypes

現在js-ctypesは開発中のため、今後仕様が変わる可能性があります。

去年行われたFirefox Developers Conference 2009でjs-ctypesについて説明しましたが、いくつかの制限があり、Firefox 3.6で使える状況はあまりなかったかと思います。現在開発を行っているFirefox 3.7では、js-ctypesが大幅に改良される予定で、現在のビルドでは、構造体のサポートやコールバック関数のサポートが含まれます。今回は構造体の使用について説明します。

この説明では、Windowsに含まれるカラーピッカー(色の選択をするダイアログ)を表示する例を挙げます。

Windowsでカラーピッカーを表示するAPIは、ChooseColorです。この関数は引数として以下の構造体のポインタを持ちます。

typedef struct {
 DWORD        lStructSize;
 HWND         hwndOwner;
 HWND         hInstance;
 COLORREF     rgbResult;
 COLORREF     *lpCustColors;
 DWORD        Flags;
 LPARAM       lCustData;
 LPCCHOOKPROC lpfnHook;
 LPCTSTR      lpTemplateName;
} CHOOSECOLOR, *LPCHOOSECOLOR;

これをctypes.StructTypeを利用して、js-ctypesで定義します。この例では、使わないメンバはuint32_tにしていますが、lpTemplateNameは文字列のポインタとして定義するのが正しいです。

var custColors_type = ctypes.ArrayType(ctypes.int32_t, 16);
const CHOOSECOLOR = new ctypes.StructType(
 'CHOOSECOLOR',
 [
  {'lStructSize': ctypes.uint32_t},
  {'hwndOwner' : ctypes.uint32_t},
  {'hInstance' : ctypes.uint32_t},
  {'rgbResult' : ctypes.uint32_t},
  {'lpCustColors' : custColors_type.ptr},
  {'Flags' : ctypes.uint32_t},
  {'lCustData' : ctypes.uint32_t},
  {'lpfnHook' : ctypes.uint32_t},
  {'lpTemplateName' : ctypes.uint32_t}
 ]);

構造体の定義が終わったら、関数を定義しましょう。定義方法は変わりませんが、引数でポインタ型を渡しています。

var comdlg32 = ctypes.open("comdlg32");
var ChooseColor = comdlg32.declare("ChooseColorW", ctypes.stdcall_abi, ctypes.int32_t, CHOOSECOLOR.ptr);

関数を呼び出すために、値をセットします。構造体のメンバにポインタを渡す必要があるため、addressを利用します。

var custColors = new custColors_type();
var col = new CHOOSECOLOR (CHOOSECOLOR.size, 0, 0, 0, custColors.address(), 0, 0, 0, 0);

値を作成したので、関数を呼び出しましょう。

ChooseColor(col.address());

そうすると、col.rgbResultにユーザーが選択した色の値がセットされますので、それを参照するとどの色を選択したかがわかります。

alert(col.rgbResult);

このように、構造体を引数に持つ関数の呼び出し方法が追加されています。これでjs-ctypesが利用できる範囲も広くなったと思います。次は、コールバック関数の利用について説明したいと思います。

なお、今回サンプルで作成したすべてのコードは以下になります。

Components.utils.import("resource://gre/modules/ctypes.jsm");

var custColors_type = ctypes.ArrayType(ctypes.int32_t, 16);
const CHOOSECOLOR = new ctypes.StructType(
 'CHOOSECOLOR',
 [
  {'lStructSize': ctypes.uint32_t},
  {'hwndOwner' : ctypes.uint32_t},
  {'hInstance' : ctypes.uint32_t},
  {'rgbResult' : ctypes.uint32_t},
  {'lpCustColors' : custColors_type.ptr},
  {'Flags' : ctypes.uint32_t},
  {'lCustData' : ctypes.uint32_t},
  {'lpfnHook' : ctypes.uint32_t},
  {'lpTemplateName' : ctypes.uint32_t}
 ]);

var comdlg32 = ctypes.open("comdlg32");
var ChooseColor = comdlg32.declare("ChooseColorW", ctypes.stdcall_abi, ctypes.int32_t, CHOOSECOLOR.ptr);

var custColors = new custColors_type();
var col = new CHOOSECOLOR (CHOOSECOLOR.size, 0, 0, 0, custColors.address(), 0, 0, 0, 0);

ChooseColor(col.address());
alert(col.rgbResult);