SyntaxHighlighter

2017年8月16日水曜日

Microsoft Edge の Native Messaging を使った拡張機能 その1 (UWP を用いた Native Messaging 拡張機能の概要)

Microsoft Edge の Native Messaging を使った拡張機能 その1 (UWP を用いた Native Messaging 拡張機能の概要)

Firefox, Chrome に続いて、 "Open TortoiseSVN" を Microsoft Edge にも移植してみたので、そのまとめです。

もう少し記事を分けるかもしれませんが、以下のような順番で書いていこうと思います。

  • その1: UWP を用いた Native Messaging 拡張機能の概要
  • その2: パッケージングの方法と、 Win32 アプリケーションの起動

なお、最終的なソースコードは GitHubの Open TortoiseSVN for Microsoft Edge にあります。

Edge の拡張機能のサポート

元々 Google Chrome の拡張機能で導入された拡張機能の形式ですが、 Mozilla Firefox にも WebExtensions として導入され、 Edge でも同様の機能がサポートされ始めていました。

具体的には、2016年の Anniversary update 以降、 Microsoft Edge も Chrome や Firefox とほぼ同様の拡張機能をサポートしています。
そして、 2017年の Creators update で、 Native Messaging のサポートも開始されました。

詳しい内容は Native messaging in Microsoft Edge のページに載っているので、基本的な作成方法などはこちらを参照するのがよいでしょう。

この記事では実際に上記のページに従って拡張機能を作成した際に、気になったところを中心に書こうと思います。

また、 Microsoft によるサンプルは SecureInput として GitHub で公開されています。

Firefox, Chrome との違い

Edge での Native Messaging サポートは、 Firefox, Chrome とは大きく違います。

Native messaging in Microsoft Edge のページに書かれているように、下記が特徴的です。

  • JavaScript で書かれた Extension は、 UWP 形式のアプリケーションとのみ通信できる。 Win32 形式のアプリケーションとは通信できない。
    Firefox, Chrome の場合は、 (Windows であれば) Win32 形式のアプリケーションと通信できる。
  • UWP アプリケーションとは、 AppService の仕組みを用いて通信を行う。
    Firefox, Chrome の場合は、標準入出力で JSON 形式のデータを所定のフォーマットでやり取りすることで通信を行う。

つまり、外部アプリケーションと通信する拡張機能の JavaScript 側のインターフェースは Firefox, Chrome と (ほぼ) 同じですが、それを受ける側であるアプリケーションの仕様は、まったく異なることになります。

なお、 Edge の Native Messaging 拡張機能の特徴として二つ書きましたが、実際には「外部のアプリケーションとは AppService の仕組みで通信を行う」という部分が重要です。
これは、 AppService は UWP アプリでしか作成できないので、必然的に UWP 形式のアプリケーションが必要条件となるためです。

UWP アプリとの通信

作成する AppService は、通常のものとまったく同じものです。なので、 UWP アプリで AppService を作成する方法については、他のページを探してみてください。
ここでは、 Edge との通信部分について書きます。

例として、仮に、 com.example.uwp という名前の AppService を作成するとします。

JavaScript 側のコード

その場合、 JavaScript 側から UWP アプリを呼び出すのは以下のようになります。

1
2
3
4
5
6
7
// JavaScript 側のコード
browser.runtime.sendNativeMessage(
    "com.example.uwp",
    {"action": "action1", "args": ["arg1", "arg2", "arg3"]},
    function(response) {
        // Do something.
    });

このように、 browser.runtime.sendNativeMessage もしくは browser.runtime.connectNative の第一引数で指定する application の名前は、 AppService の名前そのものになります。
それ以外は、 Firefox, Chrome の場合と何も違いはありません。

UWP 側のコード

AppService を実装する方法としては、 in-process な実装と、 out-of-process な実装の二種類があります。
詳しくは Microsoft の説明などを見るとよいでしょう。

Open TortoiseSVN では in-process な方法で実装したので、下記を実装する必要があります。

OnBackgroundActivated
他のアプリケーション (今回の場合は Edge) から AppService への接続があった場合、この関数が呼ばれます。
通常の AppService と同じように、この中で Connection に対してイベントハンドラを登録します。
OnAppServiceRequestReceived
他のアプリケーション (今回の場合は Edge) からメッセージが送られてきた際にそのメッセージを処理するためのイベントハンドラです。
Native Messaging のメッセージ処理の手順に合わせて実装します。

OnBackgroundActivated

OnBackgroundActivated の概要としては下記のような実装になりました。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected async override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
    base.OnBackgroundActivated(args);
    IBackgroundTaskInstance taskInstance = args.TaskInstance;
    if (taskInstance.TriggerDetails is AppServiceTriggerDetails)
    {
        var appService = taskInstance.TriggerDetails as AppServiceTriggerDetails;
        var appServiceDeferral = taskInstance.GetDeferral();
        var connection = appService.AppServiceConnection;
        connection.RequestReceived += OnAppServiceRequestReceived;
        // ...
    }
}

こちらに関しては、特に通常の AppService と違いはないはずです。

OnAppServiceRequestReceived

この関数では、 Edge からのメッセージを受け取り、それに応じて処理を行います。

概要としては、以下のようなことをすればよいです。
ここでは、上で書いた例のように JavaScript 側から {"action": "action1", "args": ["arg1", "arg2", "arg3"]} という引数で呼ぶことを考えてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private async void OnAppServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
    AppServiceDeferral messageDeferral = args.GetDeferral();
    var value = args.Request.Message.First().Value.ToString();
    var jsonSerializer = new JavaScriptSerializer();
    var valueList = (IDictionary<string, object>)jsonSerializer.DeserializeObject(value);
    var response = new ValueSet();
    switch (valueList["action"].ToString())
    {
        case "action1":
            // Key は適当でよい。
            response.Add("message", jsonSerializer.Serialize(
                new { result = true, data = "data1" }));
            break;
        default:
            // ...
            break;
    }
    await args.Request.SendResponseAsync(response);
    messageDeferral.Complete();
}

いくつか重要な点を挙げておきます。

Edge からのメッセージは ValueSet の一つ目の Value として渡される。

ValueSet は辞書のように Key, Value の組を保存できるものですが、 AppService ではこの ValueSet を用いてデータをやり取りすることになっています。そのため、 Edge の Native Messaging 拡張機能も、 ValueSet でデータをやり取りすることになります。

Edge から送られてきたデータは、 JSON 形式の文字列となって、渡された ValueSet オブジェクトの最初の (唯一の) 要素の Value に格納されてきます。

Key は "Message" になっているようですが、これを仮定しないのがよいと思います。

UWP アプリからのメッセージも同様に ValueSet として返す。

逆方向もまったく同じです。
Key は適当な名前でよいので、 Value に JSON 文字列を指定し、 SendResponseAsync でメッセージを送ります。

JSON 形式にシリアライズするためには JavaScriptSerializer と匿名型を用いるのが楽でよいと思います。

なお、本筋とは関係ないですが、やや複雑な JSON 文字列をパースする場合、以下のようにすればよいです。
例えば、 {"action": "action1", "args": ["arg1", "arg2", "arg3"]}args をパースする場合は、例えば以下のようにすればよいです。

1
2
var args = ((object[])valueList["args"])
    .Select(o => (string)o).ToArray();

このようにすることで、 "args" の中身を配列で取り出すことができます。

あとは UWP アプリ内で好きな処理をすればよいわけです。

注意点

Microsoft EdgeHTML 15.15063 の時点では、 UWP アプリからの戻り値が (私の予想と) やや異なった形式で返ってきます。

具体的には、下記のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JavaScript 側のコード
browser.runtime.sendNativeMessage(
    "com.example.uwp",
    {"action": "action1", "args": ["arg1", "arg2", "arg3"]},
    function(response) {
        // response は、 Object ではなく string
        // つまり、{"result": true, "data": "data1"} という Object ではなく
        // '{"result": true, "data": "data1"}' という string になっている。
        // よって、下記はエラー
        response["result"// Error
        // わざわざ JSON.parse する必要がある。
        response = JSON.parse(response);
        response["result"// OK
    });

このように、 Firefox, Chrome と違ってわざわざ JSON.parse する必要があります。

私はこの挙動はバグのように思いましたので、 issue として登録しましたが、まだ有効な回答はない状態です。

続いて、作成した UWP アプリを JavaScript のコードとまとめてパッケージングし、 Edge の拡張機能として登録することになります。

次回以降は、下記について書く予定です。

  • Appx パッケージの作成
  • Win32 アプリケーションの起動
  • browser.runtime.sendNativeMessagebrowser.runtime.connectNative における注意点