📓

なぜ、Unityではビルド時にC#⇒MSIL⇒C++⇒マシン語という迂遠な変換をするのか?

に公開

はじめに

Unityは2005年以降、PC、モバイル、コンソール、VR/ARといった幅広いプラットフォームに展開できる強みを持ち、世界で最も普及しているゲームエンジンのひとつとなりました。その開発言語としてC#が用いられていますが、この理由に迫ります。さらにUnityは、ビルド時にC#を直接ネイティブ化するのではなく、C# ⇒ MSIL ⇒ C++ ⇒ マシン語という一見遠回りに見えるプロセスを採用しました。これはIL2CPPと呼ばれる仕組みであり、iOSのJIT禁止ポリシーやクロスプラットフォーム展開と深く関わっています。本稿では、UnityがJavaからC#に至った経緯、C#が「スクリプト」と呼ばれる理由、iOSでのJIT禁止の背景、そしてIL2CPPの合理性を考察します。

なぜ、UnityはJavaを採用しなかったのか?

Unityの開発初期、候補に挙がっていた言語にはJavaも含まれていたと思われます。当時のJavaは「Write Once, Run Anywhere(WORA)」を掲げ、クロスプラットフォーム開発の象徴的存在でした。MacとWindowsの両方で動作する環境を目指したUnityにとって、Javaは理想的に映ったはずです。しかし現実的にはJavaの採用は困難でした。

  • Java仮想マシン(JVM)は当時のゲーム用途に必要な性能を満たせなかった
  • JVMをゲーム機やモバイル環境に搭載することは現実的ではなかった
  • サン・マイクロシステムズ(後のOracle)のライセンス制約が厳しく、Unityに自由に組み込むのは難しかった

こうした理由からUnityはJavaの採用には至らず、代替としてJavaScript風の「UnityScript」やPython風の「Boo」を採用しました。ただし製品としては初期からMonoベースのC#も選択肢として並走しており、これらスクリプト風言語は学習コストや表現力の不足から大規模開発に向かず、やがてC#が主要言語となっていきました。

C#が選ばれた理由

UnityがC#を主要言語として採用した背景には、以下の要因がありました。

  • Monoプロジェクトの存在により、オープンソースで.NET互換のランタイムを利用できた
  • C#はJavaに似た文法を持ちつつ、プロパティ、デリゲート、イベントなどゲーム開発に有用な構文を備えていた
  • Windows環境やVisual Studioとの親和性が高く、開発者がスムーズに参入できた
  • UnityScriptやBooが限界を見せ、本格的なゲーム開発言語としての要件を満たせなかった

こうしてUnityはC#を第一級の言語として位置づけ、最終的に他言語のサポートを廃止してC#に一本化しました。

なぜC#は「スクリプト」と呼ばれるのか

C#は本来コンパイル型の静的言語であり、動的に解釈されるスクリプト言語ではありません。それにもかかわらずUnityでは「C#スクリプト」と呼ばれています。その理由は以下の通りです。

  • UnityではC#コードをプロジェクトに配置すれば、エンジンが即座にコンパイルし、ゲームオブジェクトにアタッチしてすぐ利用できる。この即時性がスクリプト的に感じられる
  • 開発者はMSILやC++変換といった裏側の処理を意識せずに済み、まるでスクリプトを直接書いて動かしているような感覚で開発できる
  • 初期のUnityがUnityScriptという「JavaScript風言語」を「スクリプト」と呼んでいた流れが、C#にも引き継がれた

つまり「C#スクリプト」という呼称は技術的な正確さではなく、Unityにおけるワークフロー体験を表す言葉なのです。

iOSとJIT禁止の理由

UnityがC#を採用した後、最大の問題となったのはiOSにおけるJIT禁止ポリシーでした。AppleはApp Storeの審査ガイドラインで、動的コード生成やJITコンパイルを原則として禁止しています(2010年の厳格条項は同年秋に緩和されましたが、配布アプリにおけるJIT不可という前提は維持されています)。その理由は以下の通りです。

  • セキュリティ確保
    JITは実行時にメモリ上に機械語コードを生成し、その場で実行します。これは自己書き換えコードに等しく、悪用されれば深刻なセキュリティリスクを招きます。
  • サンドボックスの維持
    iOSアプリは厳格なサンドボックスで動作しますが、JITで生成されたコードは静的解析できず、審査時に安全性を保証できません。
  • マルウェア防止
    JITを許すと、審査時には存在しないコードを後から注入できてしまい、マルウェアや不正アプリの温床となります。

このためiOSは「すべてのコードは事前にコンパイルされていなければならない」というポリシーを採用し、JITは禁止されました。

IL2CPP誕生の合理性

iOSのJIT禁止を回避しつつ、Unityが掲げるクロスプラットフォーム戦略を維持するために生まれたのがIL2CPPです。なおAOT(事前コンパイル)自体は古くから存在し、Unityも初期のiOS向けにはMono AOT(ILからネイティブへ直接のAOT)を用いていました。IL2CPPはその上で、より広範なターゲットと最適化を狙ったAOT実装として導入されたものです。

  • C#コードをMSILにコンパイルする
  • MSILをC++コードに変換する
  • 各プラットフォーム標準のC++コンパイラで最適化されたネイティブコードを生成する

これによりUnityは、iOSを含むすべての主要プラットフォームでJITを使わずにAOT方式で動作可能になり、かつC++コンパイラの最適化技術を最大限利用できました。

なぜ直接ネイティブ化しなかったのか

「C#を直接ネイティブにコンパイルすればよいのではないか」という疑問は自然です。しかしUnityが「C# ⇒ MSIL ⇒ C++ ⇒ マシン語」という迂遠な方式を選んだのには理由があります。

  • 当時もAOT自体は存在したものの、.NETの公式的なNative AOTが現在ほど整備・成熟しておらず、Unityが独自に広範なアーキテクチャ向けのネイティブコード生成器を実装・保守するのは膨大なコストだった
  • 各CPUアーキテクチャごとのコード生成器を自作・保守するのは非現実的だった
  • C++コンパイラはほぼすべてのプラットフォームに存在し、すでに成熟した最適化技術を利用できた
  • C++を「世界共通の中間アセンブラ」として利用することで、移植性と効率性を確保できた

こうしてUnityは、最小限の開発コストで最大限の移植性を得るためにC++を経由するIL2CPPを選んだのです。

迂遠さを正当化する要素

IL2CPPは遠回りに見えますが、以下の点で合理化されています。

  • PCワークステーションの高性能化により、大規模C++ビルドも短時間で完了する
  • JITを使わないため起動時間が短縮され、挙動が安定する(プロジェクト規模や初期化の内容によっては挙動が変わることはある)
  • 動的コード生成を避けることでセキュリティが強化される
  • クロスプラットフォーム対応が容易になり、Unityの優位性を支える

歴史のIF

もし2000年代にLLVMが十分成熟していれば、Unityは「IL2LLVM」のような方式を選んでいた可能性があります。またRustが当時存在していれば、ILをRustに変換してAOTコンパイルするアプローチも考えられたでしょう。また、AOTはC++経由を必須とするものではなく、Mono AOTのようにILから直接ネイティブに落とす方式も存在していました。しかし、そうであっても、当時の現実においては、C++を経由するIL2CPPこそが最も現実的な選択でした。

まとめ

UnityはJavaについてライセンスや性能、移植性の問題から採用を断念し、MonoとC#に舵を切りました。本来スクリプト言語ではないC#が「C#スクリプト」と呼ばれるのは、Unityのワークフロー上の即時性と初期のUnityScriptの名残によるものです。その後、iOSのJIT禁止という強い制約に対応するために、UnityはC# ⇒ MSIL ⇒ C++ ⇒ マシン語という迂遠なプロセスを採用しました。

この方式は表面的には遠回りに見えますが、クロスプラットフォーム対応、セキュリティ確保、最適化活用を同時に実現する合理的な戦略でした。さらにPCワークステーションの性能向上により、この遠回りは実用上の問題ではなくなっています。付け加えると、AOT自体は古くから存在し(Unityも初期iOSではMono AOTを使用)、C++経由はUnityが選んだ実装上の最適解でした。IL2CPPは妥協の産物であると同時に、Unityの競争力を支える中核技術であり続けているのです。

※2025/10/18 一部文面を修正

Discussion