読者になるねののお庭。Clinetに強く型付けするためのライブラリをリリースしました、という話。Clinetに強く型付けするためのライブラリをリリースしました。github.comSignalRってなんぞ、っていうと、リアルタイム通信用のライブラリ。Socket.IOと似たような感じのもの、といった方が一部の人には分かりがいいかもしれない。Coreにデフォルトでビルトインされているので、お手軽にブラウザとリアルタイム通信しようと思ったらとりあえずこれ使っとけ、みたいなところがある。少なくともgRPC-webよりお手軽。SignalRはサーバ/ブラウザ間の通信だけじゃなくて、サーバ/C#クライアントapp間でもリアルタイム通信できます。できるんですが、サーバサイドは強く型付けして書けるのに、クライアント側は強く型付けできません。以下みたいな2つのインターフェースがあったとします。SomeHubMethod2();}そんな時、サーバ側では以下みたいに書けます。}}継承しているHub<T>の型引数にクライアント側のインターフェースを与えてやると、Clients.All.SomeClientMethod1(...)みたいな感じでクライアント側に定義されている関数を叩けます。それに対してクライアント側はどうか。関数名を文字列で指定してコールバックを登録しなければならない。コースバックの引数の型もOn()の型引数で指定しないといけない。ハブ側(サーバ側)のメソッドを叩くのも、やっぱり文字列で指定しなければならない。返り値の型も型引数で指定しなければならない。"message");文字列で関数名を指定!コールバックの引数の型も型引数で手動で指定!Hub側の関数の返り値の型も型引数で指定!Hub側の関数に渡す引数の型はobject型!という具合で、サーバ側の定義と睨めっこしてせっせと合わせていかないといけません。タイポは許されませんし、型違いも許されません。サーバ側の定義を変更したら、やっぱり睨めっこして合わせていかなければなりませんし、しかもコンパイル時エラーにはならないので、実行してみて初めてミスに気づく類のものです。こんな感じで嫌ポイントがたくさんあるわけです。Hub<T>の型引数のためにどうせインターフェース定義するんだからそれ活用したいじゃん、と素朴になるわけです。Clinetに強く型付けするためのライブラリを作りました。Hub側から叩かれるメソッドが定義されたReceiverを登録。Receiverの登録解除はDispose叩くだけ。subscription.Dispose();はい、非常にシンプルです。-=演算子で頑張らないといけないため、非常に面倒です。いうことで以下のようなインターフェースを用意しました。e);}このインターフェースを実装したインスタンスをRegister関数の引数に渡してやると、自動で切断/再接続などのイベントが登録されます。IHubConnectionObserverが実装されていれば、自動でそれらの関数も登録。Generatorが何をやっているか。Generatorが何をやっているかというと、まずCreateHubProxyやRegisterといったメソッドが使われているところを探します。使われているメソッドに与えられている型引数を調べて、それにあったクラスや関数を生成します。APIを通して行います。Platformと呼ばれるものの通称で、構文解析の結果やそのセマンティックなどの情報を提供してくれます。ぶっちゃけC#でアプリ書いてる分にはまったく触らないAPIですが、非常に素敵なものとなっております。TypedSignalR.ClientがRoslynで解析等してる事を列挙するとざっと以下みたいな感じです。全ソースから目的の拡張メソッドが呼ばれているところの探索TypedSignalR.ClientではCreateHubProxyとかを使ってるところ。拡張メソッドを呼んでいるクラスのチェックCreateHubProxyがHubConnectionから呼ばれているか。同一の名前空間にCreateHubProxyを別のクラスの拡張メソッドとして新規に定義することも可能なので。TypedSignalR.Clientに含まれている関数か。TypedSignalR.Client以外のところにHubConnectionの拡張メソッドとしてCreateHubProxyを定義できない事もないので。具体的には、まず以下のように関数が使われていたとします。connection.CreateHubProxy<IHubContract>();今回作成したライブラリはまずこの使用箇所を見つけます。見つけたら、型引数に与えられている型が何かを調べます。今回の場合はIHubContractです。IHubContractに定義されている関数をしらべて、以下のようにIHubContractが実装された、Hub側の関数を叩くためのクラスを生成します。HubInvokerをインスタンス化して返します。}}次にRegisterの場合ですが、以下みたいな使われ方をしていたとします。Receiver());この場合、ライブラリは型引数で与えられたインターフェースに定義されている関数を総なめして、以下のようなHubConnectionとReceiverをバインドする関数を生成します。Register関数内部ではこの生成された関数が使われます。compositeDisposable;}説明のため多少違いますが、およそこんな感じです。コンパイル時エラーちなみにこのライブラリ、いくつかの制約があります。(後ろ2つはSignalRのサーバ側からの要求なんですが)CreateHubProxy/Registerの型引数はインターフェースでなければならない。CreateHubProxy/Registerの型引数に渡されるインターフェースは関数しか定義してはいけない。Task<T>でなければならない。Receiverに使うインターフェースに定義されている関数の返り値はTaskでなければならない。上記の制約を人間が守るのは非常に大変。ランタイムエラーも極力避けたいです。ラインタイムエラー出されるくらいならコンパイル時エラーだしてくれた方が100億倍幸せです。(ちなみにサーバ側はHub<T>のTにTask以外の返り値を返す関数を実装されたものを指定するとラインタイムエラーで死にます)これらの制約を守り、ランタイムエラーを避けるため、詳細なコンパイル時エラーを出すようにしています。これでランタイムエラーとおさらば。下に表示されているエラーをタブルクリックすれば問題箇所まで飛べるので、修正も簡単。言語/構文的には問題なくても、我々開発者が「(言語的には問題ないけど)ライブラリの使い方間違えとるで」といった感じで独自のコンパイルエラーを提示する事が出来るというのは非常に素晴らしいですね。Generatorはどういうコードを生成するべきか問題。Generatorはビルドの一歩手前で実際のソースコードを生成します。生成されるソースコードをユーザが参照して叩いてもコンパイルは通り、問題なく使うことができます。これは実行時に動的コード生成する類のものではできなかったことです。Studioではインクリメンタルコンパイルが走るため、生成されたコードに対して問題なく補完が効きます。Generatorが生成したコードをユーザがバシバシ参照して叩いていいのか?という問題があります。ユーザが参照する事に何の問題があるかというと、ライブラリが抱えている制約に沿わない使い方などでコード生成にコケた場合、修正するべきところ以外までIDEが怒ってきます。それは「そんな関数はないぞ」であったり、「そんなクラスは無いぞ」であったり。問題箇所1か所さえ修正すれば良くても大量のエラーが出てくるのは良くないと思います。その1か所の特定がやっかいになったりするので。そのほかにもリファクタリングに追従できなかったり、いろいろ問題があります。いった事を避けるため、以下のような方針でTypedSignalR.Clientは実装しました。生成に絶対に失敗しないAPIをユーザに提供。(Xとします)生成に失敗する可能性がある部分は触りずらいようにしておき、原則X経由で叩いてもらうようにしておく。Generatorはコード生成するので、完全に隠蔽する事は難しいため触りずらいようにしておく、くらいで。Generator使う場合でも、普通に動的コード生成を行う時のパターンと同じような感じがいいんじゃないでしょうか。いえ生成に失敗する可能性はあるものの、生成されたコードをバシバシ触りたい、みたいな場合もあると思います。Generatorにコード生成させるための別プロジェクトを用意して、それをアプリケーション側のプロジェクトから参照する、くらいがちょうどよかったりするかもなぁとか思ってます。Clinetに強く型付けするためのライブラリを作りました。Generatorを書いてみたいけどRoslyn触ったことない人には結構参考になるんじゃないでしょうか。Generatorでメタプロっぽいこと出来ないのでRoslynは必須科目となっております。GeneratorとかAnalyzer書いてる時はRe#は切りましょう。Studioでも直近なので、Re#はまだ対応できてないみたいです。Generatorが生成したコードにジャンプできなかったりします(空っぽのファイルに飛ぶ)。実際にどうやって動いているか。Mess...リアルタイム通信するなら、やはりシリアライズ/デシリアライズ...2022-12-312022年振り返り。