ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(Unity) Android で HttpClient で通信するとインターネット権限が自動でつかない問題

筆者の動作確認: Unity 2021.3.29f1

忙しい人向けの結論

[Preserve]
internal sealed class MarkerForInternet : UnityWebRequest { }

上記のコードをどこかに書いておけば、UnityWebRequest を使っている判定になり、自動でインターネット権限をつけてくれる。

インターネット権限の付与

Unity で通信したいときは UnityWebRequest を使うと、Android の場合はビルド時に自動的にインターネット権限が付与される。 しかし、C# 標準ライブラリの HttpClient を使うと

SocketException: Access denied

のような例外が出る。あるいは、IL2CPP を通した場合、出る例外が少し違う。

WebException: Error: NameResolutionFailure

どちらにせよ、インターネット権限が自動では付与されないために発生しているようだ。

手動でインターネット権限をつけたい場合、Player Settings -> Other Settings -> Configuration -> Internet AccessAuto から Require に変更すれば権限が付く。

ここで問題が起こるのが、Unity 用のライブラリ開発者が HttpClient を使いたい場合。 パッケージ側からパッケージ利用者のアプリケーションのインターネット権限を付与することはできないため、利用者に Player Settings からインターネット権限を Require に設定してもらう必要が出てくる。これは非常に不親切。

パッケージ内にエディタ拡張などで権限を変更するような処理を書けば変更できるのかもしれないが、できればそんなことしたくない。プロジェクトの Internet AccessAuto のまま、UnityWebRequest を使った時と同じように権限が自動付与されてほしい。

解決策

考えた。 たぶん Unity のことだから UnityWebRequest を使って通信しているかどうかなんて真面目にチェックしていないだろう。UnityWebRequest クラスへの参照が存在しているかどうかで判断しているだけだろう。

[assembly: UseInternet(typeof(UnityWebRequest))]

internal sealed class UseInternetAttribute : Attribute
{
    public Type Type { get; }

    public UseInternetAttribute(Type type)
    {
        Type = type;
    }
}

上のコードをパッケージ内に書いたら、案の定パッケージを使う側のアプリに自動でインターネット権限がついた。解決。

と思ったら IL2CPP 環境で試したらインターネット権限も付与されず、解決してなかった。ストリップされてしまったのだろう。

要するに、UnityWebRequest クラスへの参照をアセンブリ内に存在させておけば、Unity は「UnityWebRequest を使っているのでインターネット権限が必要だ」と判断してくれるみたい。

ならば、UnityWebRequest クラスへの参照を含む実装がストリップによって消えなければいいのだ。 以下、IL2CPP 対応版

[Preserve]
internal sealed class MarkerForInternet : UnityWebRequest { }

UnityWebRequest を継承したクラスを定義しておいて、Preserve 属性を使って IL2CPP で消えないようにした。

これで、実際に問題なく動くことが確認できた。不要なクラスを定義せざるを得ないのは少し気持ち悪いが仕方ない。 継承クラスの定義でなくとも、UnityWebRequest が IL2CPP によって消えないコードの中で何かに使われていればいいようなので、自分なりの方法でアレンジしてほしい。

これでうまくいくのだがドキュメント記載の仕様ではないため、将来のバージョンでは使えないかもしれないので注意。

ちなみに

上記の UnityWebRequest の有無の判定が、C#コンパイル時なのかコンパイル後なのか気になった。

Source Generator などでよく使われるテクニックとして、Conditional 属性に存在しないシンボル名を書くことで、コンパイル時にのみコンパイラに情報を読み取らせてアセンブリには情報を残さないというテクニックがある。 先ほどの例に、[System.Diagnostics.Conditional("COMPILE_TIME_ONLY")] という属性をつけて、コンパイラには UnityWebRequest を認識させるがアセンブリには情報を残さないようにしてみた。

// ↓ これはインターネット権限がつかない

[assembly: UseInternet(typeof(UnityWebRequest))]

[System.Diagnostics.Conditional("COMPILE_TIME_ONLY")]
internal sealed class UseInternetAttribute : Attribute
{
    public Type Type { get; }

    public UseInternetAttribute(Type type)
    {
        Type = type;
    }
}

するとインターネット権限がつかない。コンパイル時に Roslyn でソースコードを舐めて UnityWebRequest の有無をチェックしているならこれでも権限がつくはずだが、どうやらダメなよう。 つまり、ソースコートではなくコンパイル後にアセンブリから情報を拾って UnityWebRequest の有無を見ているらしい。 なるほど、IL2CPP を通すとダメなわけだ。