もんりぃ is undefined.

育児ネタとか、技術ネタとか。

【更新あり】Zenject × IL2CPP で起きがちなエラーとその対処

更新

  • 2020/02/03 (Mon) 17:40 根本的な対処方法を教えて頂いたので追記
  • 2020/02/03 (Mon) 21:50 Unity のバージョン違いによる差異を追記
  • 2020/02/05 (Wed) 15:10 PreserveAttribute について追記
  • 2020/02/05 (Wed) 15:10 Release Notes に記載されている内容について追記

はじめに

Zenject (Extenject) 便利ですよね。大好きです。

IL2CPP 便利ですよね。凄い技術だと思います。

さて、この記事をお読みの皆様は、 Zenject (Extenject) と IL2CPP が組み合わさると、意外と大変なことになりやすい というコトをご存じでしょうか? *1

具体的には、Unity による Managed bytecode stripping with IL2CPP の地雷を思いっきり踏み抜くことが多くなるのです。

今日はその辺のお話しを少し纏めてみたいと思います。

TL; DR (2020/02/03 追記)

Unity 2019.3 を使って、コンストラクタに [Inject] 付ければ解決します!

前提

  • Scripting Backend が IL2CPP である
    • 最近だとほぼ全てのプラットフォームが該当するかな?
  • Editor では問題なく動いており、「 何故か実機でだけ動かない 」という状態である

エラーとその対処

MissingMethodException: Constructor on type '[type name]' not found.

  • Zenject とかの Reflection による動的なアレコレをやっている場合に遭遇しがち
    • 特に多いのは、Generics 型を Bind している時とか
  • このメッセージの場合は、「Constructor Injection をしたいが、そんなオーバーロードねーよ?」みたいな感じ
  • コンストラクタに限らず、 MissingMethodException が出た場合は、これの可能性が高い
MissingMethodException: Constructor on type 'Foo`1[[Bar, Buz, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]' not found.
  • Foo: クラス名
  • Bar: 第一引数の型
  • Buz: Assembly 名
internal static class Preserver
{
    private static void PreserveTypes()
    {
        {
            var _ = new Foo(default);
        }
    }
}

Attempting to call method '[method name]' for which no ahead of time (AOT) code was generated.

  • だいたい interface に生えた Generics なメソッドに値型を投げると起きるコトが多い
    • 当該メソッドが IL2CPP コンパイラによって生成されていないコトが原因
    • 拡張メソッドとして提供してたりする場合も起きがち
  • [method name] が存在するコトを教え込みましょう
Attempting to call method 'Foo::Bar.Buz<Quz>' for which no ahead of time (AOT) code was generated.
internal static class Preserver
{
    private static void PreserveTypes()
    {
        {
            var _ = default(Bar);
            _.Buz<Quz>();
        }
    }
}

2020/02/03 (Mon) 21:50 追記ここから

このエラーについては、Unity 2019.3.0f6 で改善されていることを確認しております。

public interface IFoo
{
    T Get<T>();
}

public class Bar : IFoo
{
    public T Get<T>()
    {
        // 中身はどうでも良い
        return default;
    }
}

public class Buz
{
    private IFoo Foo { get; } = new Bar();

    public void Quz()
    {
        Foo.Get<string>(); // string は参照型なので Unity 2019.2 でも Unity 2019.3 でも死なない。

        Foo.Get<int>(); // int は値型なので Unity 2019.2 だと死ぬが、Unity 2019.3 だと死なない。
        Foo.Get<DateTime>(); // DateTime も値型(構造体)なので…(ry
    }
}

実際、手元の検証プロジェクトではちゃんと動いてて、実プロダクトの方だと死んでて「なんでだー!?」って頭抱えてました。

ふと画面をみた時に、「あれ?検証プロジェクトの Unity は UI がフラット…。というコトは………???」→「っっっ!!!」という感じで会社で叫びそうになったのは内緒です。

2020/02/03 (Mon) 21:50 追記ここまで


2020/02/05 (Wed) 15:10 追記ここから

2019.3 の Release Notes にもそれっぽい記載がありました。

IL2CPP: Handle calls to open delegates on instance methods of value types properly. (1191419)

微妙に内容は違いますが、この辺の絡みで直ったんじゃないかと思われます。

2020/02/05 (Wed) 15:10 追記ここまで

Called BindInterfacesTo for type [class name] but no interfaces were found

  • 別に BindInterfacesTo に限った話しでもないけど、何らかのライブラリのクラスとかを Bind したい時に起きるコトがある
  • 基本的にライブラリ側で link.xml を提供してくれていればモウマンタイ
  • 別に Zenject 使っている場合に限った話でも無いんだけどね
Called BindInterfacesTo for type Foo but no interfaces were found
  • FooBar という Assembly に属しているモノとする
    • つまり Bar.asmdef があるってコトね
<linker>
    <assembly fullname="Bar" preserve="all"/>
</linker>
  • 細かくクラスを指定する方法もあるけど、その辺は個々に対応してくれたまへ

2020/02/03 (Mon) 17:40 追記ここから

解決方法

個別に対処するワークアラウンドは上述した通りですが、Zenject (Extenject) を用いている場合に使える魔法のような解決方法があります!

というか、Twitter@hadashiA さんに以下のリプを頂戴しました。

方法としては以下のように、消されてしまうクラスのコンストラクタに [Inject] 属性を付けるだけ、というとても簡単な方法です。*2

using Zenject;

class Foo
{
    [Inject]
    public Foo(Bar bar)
    {
        // (snip)
    }
}

The recommended fix in these cases was to add an [Inject] attribute above the constructor.

実は Extenject の README.md にも上記のように記載があったりするので、知る人ぞ知る(?)解決策のようです。

2020/02/03 (Mon) 17:40 追記ここまで


2020/02/05 (Wed) 15:10 追記ここから

Twitter@makutatsumako さんに以下のリプをもらいました。

その先のスレッドでもやりとりしているように、 [Preserve] でも何とかなりそうな気がします。

ガッツリ調査はしていませんが、理屈的にはコンストラクタに [Preserve] 付ければ良いんじゃないかと。
(当初、class に対して [Preserve] を付けて検証してしまっていたので、「駄目か…。」と早合点しておりました。)

2020/02/05 (Wed) 15:10 追記ここまで

おわりに

ここ数日、「無限に IL2CPP と格闘しているなぁ…。」とか感じてしまったので、勢いで記事を書いてみました。

「こういうケースもあるよ!」とか「こういう回避方法がオシャレだよ!」とかあれば @monry までメンションなり DM なり飛ばして貰えると、泣いて喜びます。マジで。

*1:別に Zenject 使っていなくても普通に起きるんだけど、今後自分自身の備忘録としてインデックスしたかったので Zenject と絡めて説明しています。

*2:この例は1つ目のエラー事例に於ける対処方法ですが、3つ目のエラーについても同様の対応が効くっぽいです。