xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Unity で ASP.NET Core SignalR を利用する

前回 に引き続き今回も SignalR ネタです。今回は Unity で ASP.NET Core SignalR を動かしてみようと思います。「そんなことできるんだっけ?」ともしかしたら思われるかもしれませんが、実は以下の理由によりできてしまいます!

  • Unity 2018.1 以降は .NET Standard 2.0 に対応している
  • ASP.NET Core SignalR は .NET Standard 2.0 でできている

実は ASP.NET Core SignalR は .NET Core 依存ではない、というのが特にポイントが高いところです。これにより (基本的には) Plugins フォルダに対象となる dll を入れるだけで利用できるようになります。

動かすのに必要な dll

以下の dll を Plugins フォルダに追加すれば OK です。

- Microsoft.AspNetCore.Connections.Abstractions.dll
- Microsoft.AspNetCore.Http.Connections.Client.dll
- Microsoft.AspNetCore.Http.Connections.Common.dll
- Microsoft.AspNetCore.Http.Features.dll
- Microsoft.AspNetCore.SignalR.Client.Core.dll
- Microsoft.AspNetCore.SignalR.Client.dll
- Microsoft.AspNetCore.SignalR.Common.dll
- Microsoft.AspNetCore.SignalR.Protocols.Json.dll
- Microsoft.Extensions.DependencyInjection.Abstractions.dll
- Microsoft.Extensions.DependencyInjection.dll
- Microsoft.Extensions.Logging.Abstractions.dll
- Microsoft.Extensions.Logging.dll
- Microsoft.Extensions.Options.dll
- Microsoft.Extensions.Primitives.dll
- Newtonsoft.Json.dll
- System.Buffers.dll
- System.IO.Pipelines.dll
- System.Memory.dll
- System.Runtime.CompilerServices.Unsafe.dll
- System.Threading.Channels.dll
- System.Threading.Tasks.Extensions.dll

標準の Unity では NuGet から dll を引っ張ってくる方法がありません。なので地道に必要な dll を探して追加していくことになります。地味ですがこればっかりは仕方ありません。僕は最も重要そうな dll (今回の場合 Microsoft.AspNetCore.SignalR.Client.dll) をまず追加してみて、Console に出るエラーを見ながら必要な dll をひとつずつ追加していく方法で解決していきました。

(もしかしたら NuGetForUnity という非公式のパッケージ管理システムを使えば一発解決かもしれませんが、試したことはないです...)

実際に動かしてみる

実際に動作させてみると以下のようになります。ちゃんと通知が飛んでいますね!

f:id:xin9le:20190503215724g:plain

Unity 側への通知が遅れているように見えますが、WPF アプリを手元の PC 環境で、Unity を Azure 上の VM で動作させている (= Remote Desktop で繋いでいる) ためです。実際には手元で動かすとほぼ同じタイミングで通知されるので安心してください。

IL2CPP 環境下で利用する

最近の Unity は UWP アプリを開発しようとすると「今後は IL2CPP しかサポートしないから気をつけろよ」のような警告が出ます。カジュアルに (?) こんなことを言ってきますが IL2CPP ビルドには結構ハマりポイントがある ので注意が必要です。最たるものとして IL2CPP ビルドには バイトコードストリップ という大きな特徴があります。要は静的構文解析の結果として利用されていない型は C++ コードとして展開されないというものです。

  • 明示的に型を利用しない限り消える
  • リフレクション経由でインスタンス化されているものは型を「利用していない」判定される

つまり実際には利用している型も条件次第で C++ コードとして展開されない場合があるということです。こうなると実行時エラーとなるため非常に厄介です。そしてこれは ASP.NET Core SignalR を利用するときも影響して例外ではなく、例えば実行時に以下のようなエラーが出ます。

InvalidOperationException: A suitable constructor for type 'Microsoft.AspNetCore.SignalR.Client.HttpConnectionFactory' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite (System.Type serviceType, System.Type implementationType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (Microsoft.Extensions.DependencyInjection.ServiceDescriptor descriptor, System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteChain callSiteChain) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor (System.Type serviceType) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Func`2[T,TResult].Invoke (T arg) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Collections.Concurrent.ConcurrentDictionary`2[TKey,TValue].GetOrAdd (TKey key, System.Func`2[T,TResult] valueFactory) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope serviceProviderEngineScope) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService (System.Type serviceType) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService (System.Type serviceType) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T] (System.IServiceProvider provider) [0x00000] in <00000000000000000000000000000000>:0 
  at Microsoft.AspNetCore.SignalR.Client.HubConnectionBuilder.Build () [0x00000] in <00000000000000000000000000000000>:0 

ちょっと分かりにくいかもしれませんが、IL2CPP によって型情報が欠落したため「適切なコンストラクタなくない?」と言われています。これを回避するためにはちょっとした小細工が必要です。要は IL2CPP に「その型はコンパイル時に消さないで」と指示できれば良いのですが、主に 2 種類の方法があります。

  • 特に利用しなくても良いので new でコンストラクタを呼び出しておく
  • Linker.xml で特別扱いするものをホワイトリストとして明示する

今回の ASP.NET Core SignalR の場合であれば以下の型を除外できれば OK です。こうすれば HoloLens のような IL2CPP 環境下でも ASP.NET Core SignalR を動作させることができるようになります。

- Microsoft.AspNetCore.SignalR.Client.HubConnection
- Microsoft.AspNetCore.SignalR.Client.HttpConnectionFactory
- Microsoft.AspNetCore.SignalR.Protocol.JsonHubProtocol
- Microsoft.Extensions.Logging.LoggerFactory
- Microsoft.Extensions.Options.OptionsFactory<Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions>
- Microsoft.Extensions.Options.OptionsManager<Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions>
- Microsoft.Extensions.Options.OptionsMonitor<Microsoft.Extensions.Logging.LoggerFilterOptions>

ASP.NET Core SignalR でバイトコードストリップが発生するのかと言うと、ASP.NET Core の内部で DI (= Dependency Injection) が利用されているためです。つまりリフレクション経由でインスタンス生成をしているからなのですが、これが IL2CPP と非常に相性が悪いです。なので DI を使っているようなライブラリを利用するときはバイトコードストリップに十分注意を払う必要があります。