Unity 内部のコンパイラを追ってみた

こんにちは、じぬ(@reximology)です。

これは Unity ゆるふわサマーアドベントカレンダー 2019 #ゆるふわアドカレ 24 日目の記事です。

この記事では、Unity 用 SDK 開発において遭遇したトラブルと、その解決に至るまでを紹介します。 ※ 本記事は調査手順をなぞりたいと思い時系列で記述していますが、結論だけ知りたい方は解決をご覧ください。

背景

自分は AROW というサービスの開発を行っており、その一部として Unity 用 SDK を提供しています。 SDK は、開発者登録を行うだけでなんと無料で使うことができます(これは宣伝)。

さて、SDK は提供時に DLL 化を行っています。今回紹介するのは、DLL に関連する話です。

開発環境は次のとおりです。(両刀使いのため時々 Windows が出てきますがメインは Mac です)

  • macOS HighSierra 10.13.6
  • Unity 2017.4.25
  • .NET 4.6

DLL の作り方

GUI から作ることもできますが、自動化したかったので CLI で作成することを考えます。 ドキュメント を確認し、mcs というコマンドを使うことを確認しました。 (当時どのようにインストールしたかは不明ですが、おそらく brew install mono で環境を構築したかと思います)

実際に DLL を作成するには、以下のようなコマンドを実行しています。(一部省略)

   mcs \
     -r:$(UnityDirectory)/Contents/Managed/UnityEngine.dll \
     -target:library \
     -recurse:'Assets/Scripts/*.cs' \
     -out:AROW.dll

この場合だと UnityEditor への参照は含まれていないため、Editor 用が必要な場合はもう一つ DLL を作成する必要があります。

問題

エラーとその調査

string str = "abc.def";
string[] ary = str.Split('.'); // ここでエラー!

問題になったのは、string.Split です。これは引数の文字で文字列を区切り、配列で返すメソッドです。 開発中、つまり Unity で検証している際にエラーはありませんでした。 しかし、上記処理を含むファイルを DLL 内部に含むと、次のエラーが発生するようになりました。

MissingMethodException: Method 'string.Split' not found.

正直、このエラーだけではなにが問題なのか判断できません。 同じ処理を Unity 2018 で実行した場合、もう少し詳細なエラーが確認できます。

MissingMethodException: string[] string.Split(char,System.StringSplitOptions)

コールしたいのは char を一つ受け取るメソッドですが、エラーが出ているのは別にオーバーロードされたメソッドのようです。 まず、このメソッドについてドキュメントを確認します。

使おうとしているのはこちらです。 params char[] と宣言されているため、引数が char 一つでも該当します。 逆に、エラーが示すような引数のメソッドは定義されていません。

試しに、リフレクションでメソッドを出力してみます。

// string の関数を出力
var s = "";
foreach (var v in typeof(string).GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
    s += v.ToString();
    s += "\n";
}
Console.WriteLine(s);
// Unity での実行結果
...
System.String[] Split(Char[])
System.String[] Split(Char[], Int32)
System.String[] Split(Char[], System.StringSplitOptions)
System.String[] Split(Char[], Int32, System.StringSplitOptions)
System.String[] Split(System.String[], System.StringSplitOptions)
System.String[] Split(System.String[], Int32, System.StringSplitOptions)
...

やはり、エラーとなっている char, System.StringSplitOptions を引数にとる string.Splitは見当たりません。

コンパイラ

ところが改めてドキュメントを見直すと、.NET Core にはこのメソッドが存在しています。 しかも第二引数にデフォルト値が宣言されているため、char 一つを受け取る場合は本来呼び出したかったものと見分けがつかないはずです。

つまり、「自分たちとしては params char[] のメソッドを呼び出したかったが、コンパイルされる時点では char, System.StringSplitOptions のメソッドに対して紐付けが行われた。 ただし実行時にはそのメソッドは存在しないためエラーが発生している。」と推測できます。

開発中も DLL 利用時も、実行しているのは Unity であるため共通のはずです。 となると、DLL を作成した際に利用した mcs に原因があると考えられます。 普段の Unity が利用しているコンパイラはどこにあるのでしょうか。 Unity 内で mcs に関する検索を行うと、次の場所でそれらしきファイルが発見できます。 bin には mcs、lib には mcs.exe が存在します。

// Mac
/Applications/Unity/Hub/Editor/2017.4.25f1/Unity.app/Contents/MonoBleedingEdge/bin/mcs

// Windows だと少しパスが異なる
/c/Program\ Files/Unity/Hub/Editor/2017.4.25f1/Editor/Data/MonoBleedingEdge/bin/mcs

おそらくこれなんだろうなと思いつつ、Unity から利用されている確証がほしいところです。 そこでこんな実験をします。

f:id:ryu_rand:20190819105225p:plain

Unity が使っているとおぼしきファイルをリネームしました。 後述しますが、ファイル名を変えたのは exe ファイルです。 参照するファイルがなくなっていれば多分エラーになるでしょう。 戻せば回復するでしょう。多分きっと。 ※ 元に戻せるようきちんと確認してから行ってください

f:id:ryu_rand:20190819105409p:plain

ApplicationException: Unable to find csharp compiler in /Applications/Unity/Hub/Editor/2017.4.25f1/Unity.app/Contents/MonoBleedingEdge/lib/mono/4.5
UnityEditor.Scripting.Compilers.MonoCSharpCompiler.GetCompilerPath (System.Collections.Generic.List`1[T] arguments) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/Compilers/MonoCSharpCompiler.cs:88)
UnityEditor.Scripting.Compilers.MonoCSharpCompiler.StartCompiler () (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/Compilers/MonoCSharpCompiler.cs:59)
UnityEditor.Scripting.Compilers.ScriptCompilerBase.BeginCompiling () (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/Compilers/ScriptCompilerBase.cs:43)
UnityEditor.Scripting.ScriptCompilation.CompilationTask.QueuePendingAssemblies () (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/CompilationTask.cs:191)
UnityEditor.Scripting.ScriptCompilation.CompilationTask.Poll () (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/CompilationTask.cs:137)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.CompileScripts (UnityEditor.Scripting.ScriptCompilation.ScriptAssemblySettings scriptAssemblySettings, System.String tempBuildDirectory, UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.Scripting.ScriptCompilation.EditorBuildRules+TargetAssembly[]& notCompiledTargetAssemblies) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:749)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.CompileScripts (UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.BuildTargetGroup platformGroup, UnityEditor.BuildTarget platform) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:627)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.TickCompilationPipeline (UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.BuildTargetGroup platformGroup, UnityEditor.BuildTarget platform) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:896)
UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface+<TickCompilationPipeline>c__AnonStorey3.<>m__0 () (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs:219)
UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface.EmitExceptionAsError[T] (System.Func`1[TResult] func, T returnValue) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs:75)
UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface:TickCompilationPipeline(EditorScriptCompilationOptions, BuildTargetGroup, BuildTarget)

やはりエラーになりました。ということは、Unity が利用しているコンパイラはこの mcs で間違いないでしょう。 細かい話をすると、バイナリファイル(bin/mcs)をリネームしてもエラーにはなりません。エラーになるのは実行ファイル(lib/.../mcs.exe)を対象にしたときです。 しかし mcs.exe をリネームすると、bin/mcs を直接実行した場合にもエラーが出ます。

$ /Applications/Unity/Hub/Editor/2017.4.25f1/Unity.app/Contents/MonoBleedingEdge/bin/mcs --version
Cannot open assembly '/Applications/Unity/Hub/Editor/2017.4.25f1/Unity.app/Contents/MonoBleedingEdge/bin/../lib/mono/4.5/mcs.exe': No such file or directory.

この挙動から、bin/mcs が内部で mcs.exe を利用していると判断できます。 そのため、DLL 化する際にはこの mcs を使うことで Unity 開発時と同じ環境のコンパイルができるはずです。

通常の mcs と Unity 内部の mcs を比較すると、次のような差が確認できます。

$ mcs --version
Mono C# compiler version 6.0.0.0

$ /Applications/Unity/Hub/Editor/2017.4.25f1/Unity.app/Contents/MonoBleedingEdge/bin/mcs --version
Mono C# compiler version 5.0.1.0

ドキュメントにあった .NET の差分から推察すると、新しい方の mcs では .NET Core の実装を参照していると考えられます。 (この点については未検証です)

解決

Mcs := $(UnityDirectory)/Contents/MonoBleedingEdge/bin/mcs

make_dll:
    $(Mcs) \
     -r:$(UnityDirectory)/Contents/Managed/UnityEngine.dll \
     -target:library \
     -recurse:'Assets/Scripts/*.cs' \
     -out:AROW.dll

先ほど述べたとおり、Unity 内部の mcs を利用することで今回の問題は解決できました。

開発環境の精査を怠ったことが原因となったトラブルでしたが、コンパイラについて調査するいい機会となりました。

余談

Unity 2018 の場合

Unity 2018.4 で試したところ、先ほどの mcs.exe をリネームしてもエラーが起きませんでした。 おそらく原因は、こちらにあるように Unity 2018.3 以降の .NET 4.x 系統では Roslyn が使われていることかと思われます。

Roslyn は csc を使っています。 そのため、次の csc(.exe) をリネームすると同様にエラーが発生します。

// Mac
/Applications/Unity/Hub/Editor/2018.4.0f1/Unity.app/Contents/Tools/Roslyn/csc

// Windows
C:\Program Files\Unity\Hub\Editor\2018.4.2f1\Editor\Data\Tools\Roslyn\csc.exe
Exception: 'C:\Program Files\Unity\Hub\Editor\2018.4.2f1\Editor\Data\Tools\Roslyn\csc.exe' not found. Is your Unity installation corrupted?
UnityEditor.Scripting.Compilers.MicrosoftCSharpCompiler.ThrowCompilerNotFoundException (System.String path) (at C:/buildslave/unity/build/Editor/Mono/Scripting/Compilers/MicrosoftCSharpCompiler.cs:65)
UnityEditor.Scripting.Compilers.MicrosoftCSharpCompiler.StartCompilerImpl (System.Collections.Generic.List`1[T] arguments, System.String argsPrefix) (at C:/buildslave/unity/build/Editor/Mono/Scripting/Compilers/MicrosoftCSharpCompiler.cs:112)
UnityEditor.Scripting.Compilers.MicrosoftCSharpCompiler.StartCompiler () (at C:/buildslave/unity/build/Editor/Mono/Scripting/Compilers/MicrosoftCSharpCompiler.cs:220)
UnityEditor.Scripting.Compilers.ScriptCompilerBase.BeginCompiling () (at C:/buildslave/unity/build/Editor/Mono/Scripting/Compilers/ScriptCompilerBase.cs:60)
UnityEditor.Scripting.ScriptCompilation.CompilationTask.QueuePendingAssemblies () (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/CompilationTask.cs:228)
UnityEditor.Scripting.ScriptCompilation.CompilationTask.Poll () (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/CompilationTask.cs:135)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.CompileScriptAssemblies (UnityEditor.Scripting.ScriptCompilation.ScriptAssembly[] scriptAssemblies, UnityEditor.Scripting.ScriptCompilation.ScriptAssemblySettings scriptAssemblySettings, System.String tempBuildDirectory, UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.Scripting.ScriptCompilation.CompilationTaskOptions compilationTaskOptions, UnityEditor.Scripting.ScriptCompilation.EditorCompilation+CompileScriptAssembliesOptions compileScriptAssembliesOptions) (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:1149)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.CompileScripts (UnityEditor.Scripting.ScriptCompilation.ScriptAssemblySettings scriptAssemblySettings, System.String tempBuildDirectory, UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.Scripting.ScriptCompilation.EditorBuildRules+TargetAssembly[]& notCompiledTargetAssemblies, System.String[]& notCompiledScripts) (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:1047)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.CompileScripts (UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.BuildTargetGroup platformGroup, UnityEditor.BuildTarget platform) (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:939)
UnityEditor.Scripting.ScriptCompilation.EditorCompilation.TickCompilationPipeline (UnityEditor.Scripting.ScriptCompilation.EditorScriptCompilationOptions options, UnityEditor.BuildTargetGroup platformGroup, UnityEditor.BuildTarget platform) (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilation.cs:1377)
UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface+<TickCompilationPipeline>c__AnonStorey5.<>m__0 () (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs:331)
UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface.EmitExceptionAsError[T] (System.Func`1[TResult] func, T returnValue) (at C:/buildslave/unity/build/Editor/Mono/Scripting/ScriptCompilation/EditorCompilationInterface.cs:97)
UnityEditor.Scripting.ScriptCompilation.EditorCompilationInterface:TickCompilationPipeline(EditorScriptCompilationOptions, BuildTargetGroup, BuildTarget)

Visual Studio の場合

Visual Studio で DLL を作成した場合は今回の問題は起きませんでした。 実際に実行されているコマンドは次のものです。

/Library/Frameworks/Mono.framework/Versions/5.18.1/lib/mono/msbuild/Current/bin/Roslyn/csc.exe /noconfig /nowarn:1701,1702,2008 /nostdlib+ /errorreport:prompt /warn:4 /define:DEBUG /errorendlocation /preferreduilang:ja-JP /highentropyva+ /reference:/Library/Frameworks/Mono.framework/Versions/5.18.1/lib/mono/4.7-api/mscorlib.dll /reference:/Library/Frameworks/Mono.framework/Versions/5.18.1/lib/mono/4.7-api/System.Core.dll /reference:/Library/Frameworks/Mono.framework/Versions/5.18.1/lib/mono/4.7-api/System.dll /debug+ /debug:portable /optimize- /out:obj/Debug/Temp.dll /subsystemversion:6.00 /target:library /utf8output MyClass.cs Properties/AssemblyInfo.cs "/var/folders/j2/xntwf0xj7311rg06_2p570d00000gp/T/.NETFramework,Version=v4.7.AssemblyAttributes.cs"

試したところ、mscorlib を指定することで本現象が解決できるようです。 ただし mcs で試した場合は System などで多重定義のエラーが起きるため、noconfig などのオプションが必要になります。 csc に乗り換えるリスクや、実行コマンドが複雑になることを避けるため、今回の問題解決としてはこれらの手段は採用しませんでした。