#やりたかったこと
Google Map, カレンダー, GMail, スプレッドシートなどなどのGoogleのAPIを.NET環境から利用するためのライブラリであるGoogle API .NET Clientというものがあります。これをUnityに持ってきてYouTubeのAPIをあれこれ操作しようとしてました。
#落とし穴① NuGet
このライブラリのDLLは、.NET用のパッケージマネージャーであるNuGetによって配布されてます。このNuGetは一般的なC#のプロジェクト(WindowsFormsやコンソールアプリケーション)ではVisual Studioから簡単に利用できますが、**UnityプロジェクトはNuGetに対応してません。**ライブラリがいくつものDLLに分かれているうえ、依存関係も複雑なので手動でDLLをかき集めるとなるとだいぶつらいです。今回は新規で普通のC#プロジェクトを作成しNuGetでDLLを集めてから、**直接Unityにコピーしました。**こんな手使ったのは初めてです。
##落とし穴② .NET Framework 4.6
さて、そんなこんなでDLLをAssets下に配置したわけですが、早速エラーを吐いています。実はこのライブラリ、.NET Framework 4.5以上または.NET Standard 1.3にしか対応していません。Unityはデフォルトでは.NET Framework 3.5の互換環境で動いています。
幸いなことにUnityは.NET Framework 4.6モードに変更できます。Player Settings > Other Settings > Scripting Runtime Versionを4.6に変更してエディターを再起動しますと、晴れてDLLがインポートでき……ないだと……?
##落とし穴③ System.Net.Http
本題です。
Loading Script Assembly "System.Net.Http.dll" failed!
.NET Frameworkのバージョンのエラーは消えましたが、別のエラーが出てきました。
System.Net.Http.dllはNuGetで落としてきたDLLで、Google API .NET Clientから参照されています。Google API .NET Clientのコードを見てみると、APIとの通信部分にSystem.Net.Http.HttpClientを利用しています。
ほかのDLLは無事にインポートできてるのに……なんでや!
調べたところこんな情報がありました。
どうやらUnity同梱のMonoにSystem.Net.Http.dllが存在しており、Unityのコンパイラにオプションとしてこのdllを参照するよう指定すればいいようです。
Unityのコンパイラオプションを記述するためには、Assets直下にmcs.rspというファイルを置きます。そこに
-r:System.Net.Http.dll
と記述します。ついでにNuGetから落としてコピーしたSystem.Net.Http.dllは削除しておきます。
#落とし穴④ Visual Studio Tools for Unity
Unity側のコンパイルは通るようになりました。しかし肝心のAPIの通信がうまく行きません。実はまだ落とし穴はいくつも残っていたのです。ライブラリ内部ではエラーはConsole.Log()で出力されますから、UnityのConsoleではその出力を拾えません。また、HttpClientの通信エラーの詳細はExceptionではなくHttpResponseMessageという返値で表現されるので、ライブラリからその返値を拾えない構造のせいでエラー原因がわかりません。エラー原因をアレコレ検証するうえでVisual Studioのデバッグ機能を活用することを考えました。しかしどういうわけかVisual Studioのビルドが通りません。
原因はVisual Studio側にSystem.Net.Httpの参照がないことです。それもそのはず、先ほどはUnity側のコンパイラオプションに参照を追加しただけなので、Visual Studioからはその存在を知りようもありません。
UnityのC#プロジェクトに参照を追加するには、Visual Studio Tools for Unity (VSTU)のAPIを利用する必要があります。
以下のスクリプトをAssets以下に配置します。
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
class ReferenceFixer : AssetPostprocessor
{
private static void OnGeneratedCSProjectFiles()
{
Debug.Log("OnGeneratedCSProjectFiles");
var dir = Directory.GetCurrentDirectory();
var files = Directory.GetFiles(dir, "*.csproj");
foreach (var file in files)
FixProject(file);
}
static bool FixProject(string file)
{
var text = File.ReadAllText(file);
var find = "<Reference Include=\"System\" />";
var replace = "<Reference Include=\"System\" /> <Reference Include=\"System.Net.Http\" />";
if (text.IndexOf(find) != -1)
{
text = Regex.Replace(text, find, replace);
File.WriteAllText(file, text);
return true;
}
else
return false;
}
}
VSTUがプロジェクトファイルを生成するタイミングでcsprojを操作して無理やり参照を追加します。
これでVisual Studioのデバッグ機能も利用できるようになりました。
次はHttpClientの実際のエラー内容を特定していきます。
#落とし穴⑤ TrustFailure
ExceptionをInternalExceptionで次々追っかけると、TrustFailureとかThe authentication or decryption has failedとかメッセージが出てきました。
見た通りこれはSSL周りのエラーであるようです。証明書の有効性を確認するところでつまずいているという感じです。Mono環境でしか発生しないらしい。
https://stackoverflow.com/questions/4926676/mono-https-webrequest-fails-with-the-authentication-or-decryption-has-failed
まずは通信を行う前の部分にこの文を書きます。
ServicePointManager.ServerCertificateValidationCallback = MyRemoteCertificateValidationCallback;
そのうえでこのメソッドを書きます。
public bool MyRemoteCertificateValidationCallback(System.Object sender,
X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
bool isOk = true;
// If there are errors in the certificate chain,
// look at each error to determine the cause.
if (sslPolicyErrors != SslPolicyErrors.None) {
for (int i=0; i<chain.ChainStatus.Length; i++) {
if (chain.ChainStatus[i].Status == X509ChainStatusFlags.RevocationStatusUnknown) {
continue;
}
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan (0, 1, 0);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags;
bool chainIsValid = chain.Build ((X509Certificate2)certificate);
if (!chainIsValid) {
isOk = false;
break;
}
}
}
return isOk;
}
ここまでやってやっとAPI呼び出しがうまく行きました。疲れた、、、