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.sendNativeMessage
とbrowser.runtime.connectNative
における注意点