ものがたり(旧)

atsushieno.hatenablog.com に続く

moonlight開発小話: WCFとSL2のUIスレッドモデル

ClientBase<TChannel>.InvokeAsync()の実装に至るまで、なかなかややこしい話をいくつか乗り越えてきたので、今日はその話を書くことにする。


protected void InvokeAsync(
ClientBase.BeginOperationDelegate beginOperationDelegate,
Object[] inValues,
ClientBase.EndOperationDelegate endOperationDelegate,
SendOrPostCallback operationCompletedCallback,
Object userState
)

特別に結論のある話ではなくて、要点だけをまとめるなら「SilverlightのUIスレッドモデルには気をつけるべし」といったところだろうか。UIと一見無関係なものも関係することがあるかもしれない、という話でもある。

SilverlightのUIスレッドモデル

silverlight2ではAJAXではないフツーのマルチスレッドが使えるのだけど、Windows Formsがそうであったように、UIの更新はUIスレッドで行われなければならない。SilverlightのUIは、Win32APIやX11と同様メッセージループ(イベントループ)として実装されていて、UIに対する命令はキューに格納されて逐次実行される*1

アプリケーションを作成するとき、UI上である操作を呼び出した時に、その結果が返ってくるまで、UIをブロッキングすることが、昔は普通だったし、いまでもたまにあるけど(キャンセル・中断ができない状態)、これをWebブラウザ上で行われると、その処理中は別の操作(たとえばページのスクロールとか)が全く出来ないので、けっこう困る。というわけで、Silverlightでは、同期処理は基本的に排除され、さらにはBeginFoo()みたいにEndFoo()がブロッキングするような実装にもならず、FooAsync(fooCompletedCallback)みたいに、終了コールバックを使って、処理の継続が指示できるように作られるのが、典型的なモデルだ。

WCFのClientBase

WCFのサービス クライアントは、client proxyを生成することで、サービスを表すinterfaceの呼び出しのみで全て行えるように作られている。

実際には、client proxyは以下の2種類が存在する。System.ServiceModel.ClientBase<TChannel>の派生クラスとしてのclient proxyと、ClientBase<TChannel>.Channelプロパティ値として内部的に生成されたTChannelの実装クラスである。

前者を具体的に言えば、IServiceContractFooというサービス インターフェースのクライアントは、ClientBase<IServiceContarctFoo>から派生し、IServiceContractFooを実装するクラスとなる。


class ServiceContractFooSoapClient
: ClientBase, IServiceContractFoo
{
....
}

ClientBase<TChannel>はabstractなので直接使うことができず、派生クラスを作って使用することになる。いずれにしろ、大抵の人はClientBaseの派生クラスを「Webサービスの参照」というかたちで、Visual Studioから自動生成するのだろう。.NET 3.0のsvcutil.exeを使う方法もある(trunkのmonoにもsvcutilはある)。

Visual Studioやsvcutilを使った自動生成には、WSDLが使われるので、WSDLを扱うクラスが存在しないSilverlightでは、これを行うことができない。従って、Silverlightのclient proxy生成にも、.NET 3.0が使われているはずだ。

ちなみに初期の開発版Silverlight2にはslwsdl.exeというツールがあったが、いつしか消えて無くなってしまった。まあそれは無理からぬことで、実のところVisual Studioで自動生成されているWebサービス参照は、CLR (full)を利用して生成されているはずだから、Silverlight2では実装できなかったはずである。

おまけ: ChannelFactory<TChannel>

ちなみに、WCFにはChannelFactory<TChannel>というクラスがあって、実はこのTChannelにはcontractをあらわすインターフェースを渡すようになっている。MSDNではIRequestChannelやIOutputChannelを渡すものであるかのように書かれているが、間違っているし、MSDNにはTChannelにcontractを渡す例も載っている。

このChannelFactory<TChannel>だが、CreateChannel()を用いて生成したcontractのproxyインスタンスが、Silverlightでは取得することができない。.NET 3.0では、ここではどうやらremotingのRealProxyを使っているようだが、実はSilverlightでもスタックトレースを眺めるとSystem.Runtime.Remoting.Proxies.RealProxyという文字列にお目にかかることができる。remotingの仕組みはmscorlibのAPIとしては公開されていないはずなので、何をどう実装しているのかは、ある意味興味深い。もちろん、そこまで細かい実装を知らなくても、われわれは問題なく互換環境を実現できている。

ClientBaseのAPI変更: 非同期モデル

SilverlightWCFは、.NET 3.0のWCFから膨大かつ無駄なWS-*の大部分を削り落としてスリム化した一方で、client proxyについてはけっこう作り替えている部分が多い。具体的には、それまで同期モデル中心で提供されていたAPIが、上記のような要請から、非同期モデルを前提に作り直されているのである。

SilverlightのClientBaseでは、非同期モデルのサービス呼び出しメソッドInvokeAsync()が登場している。.NET 3.0には同期モデルのメソッドも存在せず、派生クラスとして自動生成されたサービス実装クラスのサービス実装メソッドがClientBase.Channel(として内部で動的生成されたproxyクラス)のメソッドを呼び出していた。非同期モデルに様変わりしてはいるけど、Silverlightでもこれと同じことが行われている。

SilverlightにはSystem.ServiceModel.ClientBase<TChannel>.ChannelBase<T>というクラスがあって、これがClientBase.InnerChannelとして返されるcontractの実装になっている。これは完全に非同期モデルのみのAPIになっている。

monoのソースで言えば、System.ServiceModel.ClientRuntimeChannelというのがその基底クラスになって、System.ServiceModel.ClientProxyGeneratorというクラスがMono.CodeGenerationというライブラリを使ってclient proxyを生成していた。これがSilverlightになると、ClientBase.ChannelがClientBase<TChannel>.ChannelBase<T>を前提にしたコードになっていたりするので(しかも多分そのようなコードはユーザの手によるものではなく、Visual Studioで自動生成されている)、monoでも少なからず自動生成されるコードの再編成を行って、ClientBase<TChannel>.ChannelでClient<TChannel>.ChannelBase<T>の派生クラスを生成するように変更した。

BrowerHttpWebRequest

さて、ここで一見寄り道に見える話をしよう。.NET 3.0とSilverlightでは、WebRequestまわりも少なからず変更されている。一番わかりやすいのがHttpWebRequestだ。このクラスはもはやSystem.dllではabstractとしてしか定義されていない。

WebRequest.Createで生成されるHttpWebRequest(の派生クラス)のインスタンスは、System.Windows.Browser.dllの中で定義されている。このdllは、(名前から想像できるように)ブラウザ プラグインAPIIEならActiveXだろうし、moonlightではNPAPIを使っているので多分Silverlightもそうだろう)に依存する機能をまとめたものである。要するに、SilverlightのHttpWebRequestは、ブラウザのネイティブなHTTP発行命令を利用するかたちで実装されているのである。

開発版のSilverlightでは、これはSystem.Windows.Browser.Net.BrowserHttpWebRequestというpublicなクラスで実装されていたが、最終版では非公開になって、Silverlightの内部で間接的に生成されるのみになった。便宜上、本文ではこのHttpWebRequest実装をBrowserHttpWebRequestと呼ぶことにする。

BrowserHttpWebRequestの注意すべき点は、UIスレッドの制約だ。Webブラウザそのものは、Silverlight本体とは異なり、本来的にマルチスレッドをサポートしていないので、ブラウザのAPIを経由してリクエストを発行するということは、UIスレッド上にまずメッセージ(イベント)を登録し、ループのあるタイミングでHTTPレスポンスが返ってきたら、それを呼び出し元に返す、という処理になることを意味する。

BasicHttpBinding

ClientBaseのInvokeAsync()の話をする前に、もう一つ片付けておかなければならない前提がある。

.NET 3.0には数多くのTransportBindingElementが存在するが、Silverlightに存在するのはHttpTransportBindingElement(とHttpsTransportBindingElement)のみである。Bindingの選択肢も多くはないが、すべてHttpBindingElementに依存することになる。Silverlightには.NET 3.5で追加されたWebHttpBindingがなぜか含まれていないが、これもやはりHttpWebRequestを使用していた。

SiverlightのようなクライアントサイドにおけるBindingElementの役割は、ChannelFactory経由でIRequestChannelやIOutputChannelのインスタンスを提供することだ。そしてこのHttpTransportBindingElementは、(ごく自然に)前述のBrowserHttpWebRequestを使用することになる。

.NET 3.0のWCFSilverlightWCFでは、HttpTransportBindingElementのソースコード上の違いは小さいかもしれないが、そのクライアントチャネルの実行モデルは大きく異なっているのである。

ClientBase<TChannel>.InvokeAsync()をやっつける

さて、前提条件はすべて整った。以下、InvokeAsync()のやっつけ方について書こうと思う。

.NET 3.0には、同期呼び出しの他に、BeginFoo(), EndFoo()を使った非同期呼び出しのサポートも含まれている。非同期メソッドの一番簡単なやっつけ方は、非同期終了メソッドEndXxx()で同期メソッドXxx()を呼び出してしまうことだ。非同期処理ではないが、ほとんどの場合はこれで機能はする。

でもこれはInvokeAsync()については上手くいかない。なぜなら、同期メソッドでリクエストを発行してから応答を受け取るまで、そのスレッドはブロックされることになるが、同期メソッドがUIスレッドから呼び出されていれば、そのスレッドはApplicationのメッセージループに戻ることがなく、BrowserHttpWebRequestで登録されたリクエスト発行メッセージも処理されないからだ。リクエストが発行されなければ、応答を受け取ることもできない。

ひとつの正しいやっつけ方としては、同期メソッドをDelegateにして、BeginInvoke()で呼び出して、終了時にendOperationDelegateやoperationCompletedCallbackを呼び出すようにしてしまう、というものだろう(これは実際にはSilverlightのmscorlibでは不可能なので、BackgroundWorkerなどを使うことになるだろう)。InvokeAsync()に渡されるbeginOperationDelegateは、ClientBase<TChannel>.ChannelBase<T>のBeginInvoke()を呼び出すようになっているので、InvokeAsync()ではこれをそのまま呼び出せばよい。

operationCompletedCallbackの実行スレッド

これで問題なく実装完了かと思ったら、そうでもなかった。operationCompletedCallbackには、アプリケーションが実際にWCFのサービス呼び出しの完了を受けて実行されるコードが渡されており、これはUIスレッド(正確にはInvokeAsync()を呼び出したスレッド)で実行されなければならない。これを回避するには、結局UIスレッドで処理を実行するコードパスを通さなければならない。

System.Windows.dllには、System.Windows.Threading.Dispatcherというクラスがあって、これがUIスレッドのメッセージループを管理している。System.Windows.Browser.HtmlWindowのプロパティDispatcherをリフレクション経由で*2叩けば、それで上手くいくかもしれない。ちなみにInvokeAsync()はこれでは上手くいかなかったのだけど、仲間がinternalメンバのDispatcher.Mainを使えと教えてくれたので、それを使ったら上手く処理できるようになった(もちろんmoonlightの実装依存である)。

というわけで、↓こんなのもmoonlight2で動くようになりますた。


追記

ちなみに、この辺のデバッグはちっとめんどい。メインスレッド以外で行われているサービス呼び出しのエラーはunhandledだと握りつぶされるし、2.1ランタイムなのでdebuggerもAttach APIも使えないので、gdbでちまちま見るか(面倒すぎるので僕はほとんどやっていない)、Console.WriteLine()で出すしかない。まあ、WriteLineできているだけ、Silverlight上でやるよりはマシかもしれない。

*1:Silverlightの内部実装は分からないので、多少の入れ替えはあるかもしれない

*2:というのもSystem.ServiceModel.dllをわれわれがビルドする時には、System.Windows.dllもSystem.Windows.Browser.dllも参照できないので