Unity において record を用いて開発していたところ、突然 Android ビルド時にエラーが発生して失敗するようになった際の調査と対応の記録です。
私のプロジェクトではマスターデータの定義に record を使用しています。詳細は割愛しますが、不変 (immutable) であることや記述量の少なさの特徴から、動的に内容が変わることがないマスターデータの定義に適していると判断して採用しています。
環境
- Unity 2022.3.23f1
- Unity Build Automation
- Scripting Backend: IL2CPP
エラー内容
開発中の動作確認のために Android ビルドを回したところ、以下のエラーが発生してビルドに失敗しました。
BUILD_PATH/p\Library\Bee\artifacts\Android\il2cppOutput\cpp\App__31.cpp(5039,4042): fatal error: bracket nesting level exceeded maximum of 256
BUILD_PATH/p\Library\Bee\artifacts\Android\il2cppOutput\cpp\App__31.cpp(5039,4042): note: use -fbracket-depth=N to increase maximum nesting level
カッコのネストが深すぎるというエラーで、そんな記述をしているところはないはず… と思っていましたが、よく見ると IL2CPP の出力ファイルで起きているものでした。
clang コンパイラではカッコのネスト数に制限が設けられており、その既定値が 256 であるようです。これを超えてしまったということ…?
-fbracket-depth=N
Sets the limit for nested parentheses, brackets, and braces to N. The default is 256.
原因
上記エラーは Unity が提供するクラウドビルドサービスの Unity Build Automation で発生したものですが、作業 PC でビルドすることでも再現しました。原因となっているファイルを見てみると…
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR int32_t SettingsMaster_GetHashCode_mFC42BFF00C22982EA188BFA2860E9480138D3E2B (SettingsMaster_tAEDA995EE558CE6A0365D3FD559756267BC2E23C* __this, const RuntimeMethod* method)
{
static bool s_Il2CppMethodInitialized;
if (!s_Il2CppMethodInitialized)
{
il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&EqualityComparer_1_get_Default_m5940F55379DF8F2C0717CB910CAA32DD9E69F06E_RuntimeMethod_var);
il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&EqualityComparer_1_get_Default_mBA32E77AE7449FD3D831FCAC13316F47FF72322B_RuntimeMethod_var);
il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&EqualityComparer_1_get_Default_mF70F6C11A35B420DFA4628EE316B087F2DCB280C_RuntimeMethod_var);
s_Il2CppMethodInitialized = true;
}
{
EqualityComparer_1_t83FDE9B1E4980E1A7341C2B8DDCD10212FB1F928* L_0;
L_0 = EqualityComparer_1_get_Default_mBA32E77AE7449FD3D831FCAC13316F47FF72322B_inline(EqualityComparer_1_get_Default_mBA32E77AE7449FD3D831FCAC13316F47FF72322B_RuntimeMethod_var);
Type_t* L_1;
L_1 = VirtualFuncInvoker0< Type_t* >::Invoke(5, __this);
NullCheck(L_0);
int32_t L_2;
L_2 = VirtualFuncInvoker1< int32_t, Type_t* >::Invoke(9, L_0, L_1);
// ...
return ((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(((int32_t)il2cpp_codegen_add(((int32_t)il2cpp_codegen_multiply(L_2, ((int32_t)-1521134295))), L_5)), ((int32_t)-1521134295))), L_8)), ((int32_t)-1521134295))), L_11)), ((int32_t)-1521134295))), L_14)), ((int32_t)-1521134295))), L_17)), ((int32_t)-1521134295))), L_20)), ((int32_t)-1521134295))), L_23)), ((int32_t)-1521134295))), L_26)), ((int32_t)-1521134295))), L_29)), ((int32_t)-1521134295))), L_32)), ((int32_t)-1521134295))), L_35)), ((int32_t)-1521134295))), L_38)), ((int32_t)-1521134295))), L_41)), ((int32_t)-1521134295))), L_44)), ((int32_t)-1521134295))), L_47)), ((int32_t)-1521134295))), L_50)), ((int32_t)-1521134295))), L_53)), ((int32_t)-1521134295))), L_56)), ((int32_t)-1521134295))), L_59)), ((int32_t)-1521134295))), L_62)), ((int32_t)-1521134295))), L_65)), ((int32_t)-1521134295))), L_68)), ((int32_t)-1521134295))), L_71)), ((int32_t)-1521134295))), L_74)), ((int32_t)-1521134295))), L_77)), ((int32_t)-1521134295))), L_80)), ((int32_t)-1521134295))), L_83)), ((int32_t)-1521134295))), L_86)), ((int32_t)-1521134295))), L_89)), ((int32_t)-1521134295))), L_92)), ((int32_t)-1521134295))), L_95)), ((int32_t)-1521134295))), L_98)), ((int32_t)-1521134295))), L_101)), ((int32_t)-1521134295))), L_104)), ((int32_t)-1521134295))), L_107)), ((int32_t)-1521134295))), L_110)), ((int32_t)-1521134295))), L_113)), ((int32_t)-1521134295))), L_116)), ((int32_t)-1521134295))), L_119)), ((int32_t)-1521134295))), L_122)), ((int32_t)-1521134295))), L_125)), ((int32_t)-1521134295))), L_128)), ((int32_t)-1521134295))), L_131)), ((int32_t)-1521134295))), L_134)), ((int32_t)-1521134295))), L_137)), ((int32_t)-1521134295))), L_140)), ((int32_t)-1521134295))), L_143)), ((int32_t)-1521134295))), L_146)), ((int32_t)-1521134295))), L_149)), ((int32_t)-1521134295))), L_152)), ((int32_t)-1521134295))), L_155)), ((int32_t)-1521134295))), L_158)), ((int32_t)-1521134295))), L_161)), ((int32_t)-1521134295))), L_164)), ((int32_t)-1521134295))), L_167)), ((int32_t)-1521134295))), L_170)), ((int32_t)-1521134295))), L_173)), ((int32_t)-1521134295))), L_176)), ((int32_t)-1521134295))), L_179)), ((int32_t)-1521134295))), L_182)), ((int32_t)-1521134295))), L_185)), ((int32_t)-1521134295))), L_188)), ((int32_t)-1521134295))), L_191)), ((int32_t)-1521134295))), L_194)), ((int32_t)-1521134295))), L_197));
}
}
長さの都合で途中を端折っていますが、 return の値がとんでもないことになっています。
SettingsMaster は汎用的なパラメータを持つ、record として定義されたマスターデータです。 GetHashCode という名が見えたことで、あることを思い出しました。
record の特性の一つとして、 GetHashCode や Equals などのメソッドを自動生成するというものがあります1。実際にどのようなコードが生成されるのかを把握していなかったのですが、少なくとも GetHashCode は record に定義したプロパティの数に比例してネストが増えてしまっているように見えます。
このときに定義していた record は以下のような形で、プロパティ数は実に 65 個存在していました。
public record SettingsMaster(
[property: JsonProperty("xxx"), JsonRequired]
int XXX,
[property: JsonProperty("yyy"), JsonRequired]
float YYY,
// ...
);
解決方法
この問題の解決方法として、以下が挙げられます。
- ネストの上限を引き上げる
-
GetHashCodeメソッドを override する - record の使用をやめる
それぞれ見ていきます。
ネストの上限を引き上げる
エラー内容にもあったように、clang コンパイラに -fbracket-depth オプションを指定して上限を引き上げれば解決しそうです。
しかし、この問題はプロパティの数に応じて肥大化することで発生し、この対応では一時しのぎに過ぎないためパスです。
試していませんが、Unity ではエディタスクリプトでビルドの前処理として以下のようなコードを実行すればオプションを設定できるはずです。 512 の箇所は適宜読み替えてください。
PlayerSettings.SetAdditionalIl2CppArgs("--compiler-flags=\"-fbracket-depth=512\"");
GetHashCodeをoverrideする
肥大化しているメソッドを短いコードで override してしまえば解決するので、この中では一番簡単に対応できるものかと思います。
やるとしたらこんな感じでしょうか。タプルの方は記述量が少なく済みますが、もしかしたら生成されるコードが大きくなるかもしれません。
public override int GetHashCode()
{
var hc = new HashCode();
hc.Add(XXX);
hc.Add(YYY);
// ...
return hc.ToHashCode();
}
public override int GetHashCode() =>
(XXX, YYY, /* ... */).GetHashCode();
ただ、これだとプロパティが増減するたびに変更しなくてはいけません。また、適当な値を返すのではメソッドとしての意味がなくなるのでこちらもパス。
recordの使用をやめる
2 つの record を比較するとき、既定ではすべてのプロパティとフィールドの値が同じであれば等しいと判定されます2。しかし、マスターデータのような参照にしか使わないケースにおいてはそもそも比較することがありません。
つまり、record での定義に固執せず class などにすることで解決できます。
今回のケースでは class に変更することが堅実に解決できるということで、こちらを採用しました。定義は以下のようになりました。
public class SettingsMaster
{
[JsonProperty("xxx"), JsonRequired]
public int XXX { get; init; }
[JsonProperty("yyy"), JsonRequired]
public float YYY { get; init; }
// ...
}
まとめ
今回のビルドに失敗する問題は record の特性による思わぬ落とし穴でした。
record は便利なのですが、IL2CPP を使用していてプロパティが大量にあるケースでは注意が必要ですね (他のマスターデータ定義は DB のカラムに基づいてプロパティを設けているので、上記の SettingsMaster 自体例外的なものではありますが)。