[Firefox OS][FxOS][Gecko] webidlでのAPIの追加

 藪下@2課のガジェオタです。

 しばらくブログサボってたんですが書くこと作っていかないとなーと思ってたところにいい感じの質問を貰ったのでやってみます。
 ご質問くださったリスナーの方はAPIを自前で作りたいとのことなのでAPIの追加について書きます。
 本ブログにはすでにAPIの追加について記事があるんですが、内容が少々古くなっているので最近のやり方について調べてみました。

目次

  • FxOSの大まかな構造
  • WebIDL
  • APIの追加
    • WebIDLの追加
    • バインディングエントリの追加
    • 実装の追加
  • テストアプリ
  • まとめ

FxOSの大まかな構造

 FxOSのAPIは大まかにいうと以下の構造になっています。

レイヤー 役割 使用言語
Gaia アプリ、UI HTML、JavaScript
Gecko ランタイム JavaScript、C/C++
Gonk OS、HAL C/C++

 今回の記事ではGecko層にAPIを追加するのが目的になります。
 リスナーさんからの質問ではデバイスの値をもっと精密に取りたいという話だったので次回以降の記事でGonk層も触ります。

WebIDL

 以前の記事ではAPIの追加にXPIDLを使っていたんですが、最近のFxOSではWebIDLというのを使います。
 WebIDLが何なのかざっくりと説明すると、ブラウザへの追加機能をJavaScriptなどとバインディングする方法をW3Cで定義したものです。JavaScriptなどへのAPIの提供の際にどのようなAPIを提供するのかの示し方を標準化したものとも言えます。
 WebIDLについてちゃんとした文書はW3Cを参照してください。

Web IDL(W3C):http://www.w3.org/TR/WebIDL/
Web IDL(日本語訳):http://www.hcn.zaq.ne.jp/___/WEB/WebIDL-ja.html

 FxOSでのちゃんとした情報についてはMDNを参照してください。

MDN(英語):https://developer.mozilla.org/en-US/docs/Mozilla/WebIDL_bindings
MDN(日本語):https://developer.mozilla.org/ja/docs/Mozilla/WebIDL_bindings

 この記事を書いている時点で日本語の方は翻訳中でさわりの部分だけ日本語になっています。

APIの追加

 ここからはAPIの追加の仕方を説明します。まず大筋を説明すると

      WebIDLの追加
      バインディングエントリの追加
      実装の追加

 が必要になります。以下順番に説明します。

WebIDLの追加

 WebIDLはすべて gecko/dom/webidl 配下に置かれます。
 ここに新しくHelloAPI.webidlを置きます。中身は以下の通りです。

[Constructor()]
interface HelloAPI {
    DOMString hello();
};

 作ったHelloAPI.webidlをdom/webidl/WebIDL.mkのリストに追加します。webidl_filesに追記しましょう。

webidl_files = \
  AudioBuffer.webidl \
    :
  (snip)
    :
  XMLHttpRequestUpload.webidl \
  HelloAPI.webidl \
  $(NULL)

バインディングエントリの追加

 実際のコードとwebidlで定義した内容をバインディングするための設定を書きます。
 dom/bindings/Bindings.confに以下のような設定を追加します。詳しい内容はMDNを参照してもらうとして、ここでは実際のクラス名を指示しています。
 これに沿ってHelloAPIBinding.h/.cppが生成されます。

DOMInterfaces = {

'HelloAPI' : {
    'nativeType': 'HelloAPIImpl',
},

'mozAudioContext': {
    'nativeType': 'AudioContext',
    'implicitJSContext': [ 'createBuffer' ],
},
    :
  (snip)
    :
}

実装の追加

 今回はgecko/dom配下にhelloディレクトリを用意して実装ファイルを置いていきます。のつもりでしたがどうにもリンクが通せなかったのでずぼらしてHelloAPI.cppとHelloAPI.hをgecko/dom/baseに置きます。
 まずgecko/dom/baseにソースコードがあることを示さなければならないのでgecko/dom/base/Makefile.inにファイル名を追加します。

EXPORTS_NAMESPACES = mozilla/dom
EXPORTS_mozilla/dom = \
    :
  (snip)
    :
  HelloAPIImpl.h \
  $(NULL)

CPPSRCS =			\
    :
  (snip)
    :
	HelloAPIImpl.cpp \
	$(NULL)

 次にファイルの用意。HelloAPI.cppとHelloAPI.h、Makefile.inをgecko/dom/baseに配置します。
 詳しくはMDNを見てもらうとしてポイントだけ見ていきます。

HelloAPI.h

 まずはヘッダ全体。

#ifndef HELLO_API_H
#define HELLO_API_H

#include "nsWrapperCache.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIDOMWindow.h"
#include "mozilla/Attributes.h"
#include "nsAutoPtr.h"
#include "nsString.h"
#include "mozilla/ErrorResult.h"
#include <android/log.h>

namespace mozilla {
namespace dom {

class HelloAPIImpl : public nsISupports,
                     public nsWrapperCache
{
public:
    HelloAPIImpl(nsIDOMWindow* aWindow): mWindow(aWindow)
    {
        __android_log_print(ANDROID_LOG_INFO, "HelloAPIImpl", "HelloAPIImpl::HelloAPIImpl(nsIDOMWindow*) S");
        SetIsDOMBinding();
        __android_log_print(ANDROID_LOG_INFO, "HelloAPIImpl", "HelloAPIImpl::HelloAPIImpl(nsIDOMWindow*) E");
    }

    virtual ~HelloAPIImpl() {}

    static already_AddRefed<HelloAPIImpl>
    Constructor(nsISupports*& aGlobal, ErrorResult& rv)
    {
        return already_AddRefed<HelloAPIImpl>(new HelloAPIImpl(nullptr));
    }

    void Hello(nsString& ret)
    {
        // hello, world!を出力する
        nsString* tmp = new nsString((nsString::char_type*)L"hello, world!");
        ret = *tmp;
    }

    // WebIDL required
    JSObject* WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap);

    nsIDOMWindow* GetParentObject() const
    {
        return mWindow;
    }

    // nsISupports required
    NS_DECL_CYCLE_COLLECTING_ISUPPORTS
    NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HelloAPIImpl)

    static bool HasSupport();
    void Shutdown();

private:
    nsCOMPtr<nsIDOMWindow> mWindow;
};

} // dom
} // mozilla

#endif // HELLO_API_H

 まずはclassの宣言ですが、追加するinterfaceに対応するclassはnsISupportsを継承することが多いようです。これはDOM APIに関係したオブジェクトのツリーを作ったり他のオブジェクトやメソッドに転送するための抽象化のようです。
 そしてオブジェクトを沢山作ったり素早く作ったりするのにパフォーマンスを気にするならnsWrapperCacheの継承が必要です。これは生成したオブジェクトをキャッシュする仕組みに組み込むためのものです。今回必要ないんですがMDNの説明で組み込んでいるのでとりあえず継承しときました。

class HelloAPIImpl : public nsISupports,
                     public nsWrapperCache

 少し下に飛びますがnsISupportsの提供するインターフェイスを実装する必要があるので用意していきます。
 nsISupportsについてはマクロが用意されているのでそれを使います。
 addRefやreleaseなどの基本的なヘルパ関数はこのあたりのマクロから展開されます。xxxBindings.h/cppでaddRefがないとか出てきたらこのマクロが展開されてません。マクロをpublicなメンバを各位置に書いているか、nsISupports.hを正しくインクルードしているかなど気にするといいでしょう。

    // nsISupports required
    NS_DECL_CYCLE_COLLECTING_ISUPPORTS
    NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HelloAPIImpl)

 次にnsWrapperCacheが要求するインターフェイスを満たします。
 ずぼらしてヘッダだけでいけないかなと思っていた名残でいくつかの関数がクラス定義内に実装されてますが、WrapObjectは#includeの循環とかでヘッダ側に書けませんでした。実装はMDNにある通りxxxBindings::Wrapに転送するだけですがcpp側で説明します。

    // WebIDL required
    JSObject* WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap);

 DOMの親子関係を辿れるように親オブジェクトを取得する関数を用意します。これはnsISupportsやnsWrapperCacheに関係なく必要なようです。

    nsIDOMWindow* GetParentObject() const
    {
        return mWindow;
    }

 最後にwebidlで定義したinterfaceから転送されてくるメソッドを用意します。
 今回のHelloAPIではコンストラクタ属性を付けたのとhello()メソッドを宣言したのでConstructorとHelloの2メソッドが必要です。このあたりの命名規則がよくわかってないのですが、メソッド名の頭文字は大文字にされるようです。名前がおかしいとxxxBindings.cppのコンパイルでエラーが出るのでそれに合わせます。
 webidlでの宣言に対するcpp側のシグネチャについてはMDNを参照してください。ここでは使ったものだけ説明します。
 Constructorについてはグローバル (ルート) オブジェクト、Constructor属性で指定した型に対応する方の引数リスト、例外を返すための参照という決まりです。今回は無引数のConstructorなのでグローバル (ルート) オブジェクトとErrorResult&です。

    static already_AddRefed<HelloAPIImpl>
    Constructor(nsISupports*& aGlobal, ErrorResult& rv)
    {
        return already_AddRefed<HelloAPIImpl>(new HelloAPIImpl(nullptr));
    }

 hello()ではDOMStringを返却するよう宣言しています。cpp側では返り値ではなく引数経由で返します。DOMStringに相当するクラスがnsStringになるのでnsString&の引数を持つ関数を定義します。

    void Hello(nsString& ret)
    {
        // hello, world!を出力する
        nsString* tmp = new nsString((nsString::char_type*)L"hello, world!");
        ret = *tmp;
    }

HelloAPI.cpp

 次にソース全体。

#include "HelloAPIImpl.h"
#include "mozilla/dom/HelloAPIBinding.h"
#include "nsContentUtils.h"
#include <android/log.h>
#include "mozilla/Preferences.h"

namespace mozilla {
namespace dom {

NS_IMPL_CYCLE_COLLECTION_CLASS(HelloAPIImpl)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HelloAPIImpl)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mWindow)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
  NS_DROP_JS_OBJECTS(tmp, HelloAPIImpl);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HelloAPIImpl)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mWindow)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(HelloAPIImpl)
  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(HelloAPIImpl)
NS_IMPL_CYCLE_COLLECTING_RELEASE(HelloAPIImpl)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HelloAPIImpl)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

JSObject* HelloAPIImpl::WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap)
{
    return HelloAPIBinding::Wrap(aCx, aScope, this, aTriedToWrap);
}

/* static */ bool HelloAPIImpl::HasSupport()
{
  return Preferences::GetBool("dom.helloApi.enabled", true);
}

void HelloAPIImpl::Shutdown()
{
}

} // dom
} // mozilla

 こちらでもnsISupportsとnsWrapperCacheの実装を行っていきます。
 これもマクロが用意されているので使っていきます。これはメンバ関数になるので名前空間で囲まれている場所に書きましょう。
 それぞれのマクロの説明は長くなるので省きますが、クラスに対しての細工とメンバ変数に対しての細工があるのでそれぞれ気を付けて書いていきます。基本的にクラス名が必要なものはメンバ関数の宣言部分。メンバ変数が必要なのはスマートポインタの管理コードなどです。
 nsWrapperCacheを継承する場合はNS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTIONあたりのマクロを書く必要があります。

NS_IMPL_CYCLE_COLLECTION_CLASS(HelloAPIImpl)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HelloAPIImpl)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_NSCOMPTR(mWindow)
  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
  NS_DROP_JS_OBJECTS(tmp, HelloAPIImpl);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HelloAPIImpl)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_NSCOMPTR(mWindow)
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(HelloAPIImpl)
  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(HelloAPIImpl)
NS_IMPL_CYCLE_COLLECTING_RELEASE(HelloAPIImpl)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HelloAPIImpl)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

 WrapObjectの実装です。前述したとおりxxxBindings::Wrapへの転送のみ行ってます。

JSObject* HelloAPIImpl::WrapObject(JSContext* aCx, JSObject* aScope, bool* aTriedToWrap)
{
    return HelloAPIBinding::Wrap(aCx, aScope, this, aTriedToWrap);
}

 他の関数はwindowオブジェクトやwindow.navigatorにぶら下げたい場合に使うので実装していますが、new Xxx()して利用する場合には必要ないので割愛します。

テストアプリ

 追加したインターフェイスが使用できるならnew HelloAPI()したオブジェクトのhello()を呼べばhello, world!という文字列が得られるはずです。
 ということでテストアプリ。

<!DOCUTYPE html>
<html>
<head>
    <meta charset=UTF-8>
    <title>hello test</title>
    <script>
        function log(elem, log) {
            elem.appendChild(document.createTextNode(log));
            elem.appendChild(document.createElement("br"));
        }

        function helloPrint() {
            var elem = document.getElementById("hoge");
            var helloApi = new HelloAPI(window);
            if (helloApi == null) {
                log(elem, "helloApi is null\n");
            } else {
                var hello = helloApi.hello();
                if (hello == null) {
                    log(elem, "hello is null\n");
                    for (var key in hello) {
                        log(elem, hello[key] + "\n");
                    }
                } else {
                    log(elem, hello + "\n");
                }
            }
        }
    </script>
</head>
<body onload="helloPrint()">
    <p id="hoge">
    </p>
</body>
</heml>

 結果のスクショ。

結果のスクショ

 あれ?

まとめ

 webidl書いてビルドエラー見ながらAPIの実体書いてnsISupportsとかnsWrapperCacheのマクロペタペタすればとりあえずはAPI追加できました。
 ちなみに藪下は結構長いことnewでエラーになるようってなってましたがコンストラクタ属性宣言するだけでした。コンストラクタ属性つけない場合はどっかのオブジェクトにぶら下げましょう (この辺マクロいっぱい出てきてちょっとよくわかりませんでした)。

 最後ちょっと (だいぶ?) ミスったとか結局ディレクトリ作ってソース入れてMakefile書いたときのリンク時に.o見つけてくれないとか未解決部分があるのでまたそのうち調べたいと思いますが、とりあえずちょっとAPI追加するだけならそんなに大変ではないようです。
#藪下はビルドの仕組み調べてさまよってましたが。。。

 近いうちにgonk層も触ってみようかと思っているのでその際にこのあたりももう一度触れます。コールバックメソッドを受け取るプロパティを持ったインターフェイスとかはまだちゃんと説明を読んでないのでそこら辺とgonk層の構造あたりをかけたらいいなと思います。

[LeapMotion]LeapMotionを使ってみた

今話題?のLeapMotionですが、WindowsXPに対応していないようです。
SDKもリリースされていますが、こちらもWindowsで対応しているのは7,8のみ。
そこで、何とかWindowsXPでLeapMotionを動かしてみました。

以降、VC#でサンプルコードをコンパイルし実行するまでの手順を紹介します。

当環境は以下の通りです。
WindowsXP Pro SP3
Visua C# 2010

1.SDKの導入
 LeapMotionSDKをVC#に導入する方法は、以下の記事で丁寧に紹介されています。
 Leap Motion Developer SDK で開発できる環境を整える(Windows C# 編)

 実行すると例外「TypeInitializationException」が発生する場合は、
 「LeapCSharp.dll」と「Leap.dll」を実行ファイルと同じフォルダに置いてみて下さい。
 また、まだPCにLeapMotionを接続出来ていないため、
 アプリケーションを実行しても、LeapMotionからの入力は表示されません。

2.LeapMotionControllerのインストール
 下記サイトからLeapMotionControllerをダウンロードし、実行します。
 https://www.leapmotion.com/setup

 WindowsXPの場合、以下のようなダイアログが表示されます。
 Airspace等はXPに対応していないため、CoreServiceのみインストールされるようです。
 LeapMotionControllerSetup
 OKを押すとダイアログが消えますが、バックグラウンドでインストールが行われています。

3.LeapServiceの起動
 WindowsXPの場合、LeapServiceを手動で起動する必要があるようです。
 コマンドプロンプトを起動し、以下のコマンドを実行し下さい。
 「net start LeapService」

 自前のアプリを動作させるだけであれば、アクティベートは必要ないようです。

4.サンプルコードの実行
 これでLeapMotionを認識するはずなので、サンプルコードを再度実行してみましょう。
 今度はLeapMotionからの入力データが表示されたと思います。

これでWindowsXPしか使えない場合も、LeapMotion用アプリの開発はできそうです。