インサイドXamarin(3)

インサイドXamarin(3)

Xamarinの基盤「Mono」のmonoランタイムとクラスライブラリ

2016年9月16日 改訂 (初版:2014/1/27)

Xamarinにおけるソフトウェアの基盤であるMonoを深く理解すれば、Xamarin製品の理解はもっと深まる。今回はmonoランタイムと、Monoのクラスライブラリについて解説する。

榎本 温(@atsushieno
  • このエントリーをはてなブックマークに追加

 前回は、Monoの成り立ちから、そのソフトウェア構成、C#コンパイラーの内容まで、Monoについて解説した。今回も引き続き、monoランタイムと、Monoのクラスライブラリについて解説する。

mono: ECMA CLIランタイム

 次はmonoランタイムについて説明しよう。monoランタイムは、.exeファイルや.dllファイルを解釈して、そのCIL(MSIL)コードをCPUのネイティブ命令に置き換えて実行する、実行エンジンだ。その中身は、CIL(MSIL)メタデータローダー、JITコンパイラーなどの実行エンジン、メモリ管理(ガベージコレクション)、AppDomainやリモーティング、スレッド管理、I/O処理、P/Invokeの実現、デバッガーのサポートなど、多岐にわたる。

アセンブリのロードと実行

 monoがCILのコードを実行する方式として一番簡単なのは、EXE形式のCILコードの実行だ。EXEcutable(=実行形式)としてのmono(Windowsならmono.exe)は、EXE形式のMSILコードを実行するためのコンソールツールだ。例えば次のコマンドは、実際にexecutableとしてのmonoをコンソールで実行している。

コンソール
$ mono MyProgram.exe # MyProgram.exeを実行する
monoコマンドによるプログラムの実行

 .NETの.exeファイルは、Windows上ではネイティブの実行ファイル形式(Coff)に収まっているが、Windows以外のOSではそうではない。CILはどう逆立ちしてもLinuxの標準実行ファイル形式であるELFのフォーマットには適合しない。ECMA CLIはこの点では明らかにWindowsに有利な仕様を策定している。「monoで引数に実行ファイルを指定して実行するしかないのは不便だ」と言うのはお門違いだ(Linuxでは「binfmt_misc」という機能を使って、.NET形式のバイナリをmonoの呼び出しに関連付けることはできる。EXE形式はWindowsのネイティブ実行ファイルで、Wineに実行してもらわないと使えない可能性もあるから、Linux上で拡張子やMIMEタイプで関連付けるのはお勧めできない)。

 .NETのアプリケーションは、EXE形式のエントリポイントを持つとは限らない。例えば、ASP.NETのWebアプリケーションは、DLL形式で生成され、ASP.NETの実行エンジン(IISならISAPI)が、独自にCLRあるいはmonoのホスティングAPIを使用して、そこに独立したアプリケーションドメインを作成し、そこにdllをロードして、所定のエントリポイントからアプリケーションを実行する。Silverlightなら、CoreCLR*1と呼ばれるホストが、xapアプリケーションパッケージから、マニフェストに基づいてdllを探し出して、それをロードしていた。

  • *1 2015年に公開された.NET Coreのランタイムに、「CoreCLR」という同じ名前が再利用されている。2016年現在でCoreCLRと言ったら、こちらを指すものと考えた方がよい。

 同じことが、monoについても当てはまる。monoの実行エンジンの本体は、「libmono」というライブラリで独立していて、これを使用すれば独自のホストを作成できる。このlibmonoのAPIは、「monoの組み込みAPI」、あるいは「embedded mono」と呼ばれる。典型的な利用例は、ブラウザープラグインだ。Unityは最も有名なembedded monoのユーザーといえるが、彼らは「Web Player」というランタイムプラグインを提供している。Monoチームでも、かつて「Moonlight」というSilverlight互換環境を公開していたが、これもブラウザープラグインAPIの標準的存在であるNPAPIを経由して、monoランタイムを起動するプラグインだ。グーグルは、「NaCL(native client)」というブラウザー用ネイティブコード実行環境で、monoを動作させるためのパッチを、monoにコントリビュートしていた。monoを動かせるNaCLでは、Unityを動かすこともできる。

 monoランタイムは、アセンブリのCILメタデータをロードして、環境変数や構成ファイルと照らし合わせて、適切なmscorlib.dllファイルをロードし、.NETランタイムのプロファイルを選択する。ロードされるmscorlib.dllのバージョンによって、そのランタイムのプロファイル(.NET 2.0/.NET 4.0/.NET 4.5/iOS/Androidなど)が決まる。.NET 2.0のプロファイルは公式にはもうサポートしない、いつ消えてもおかしくないプロファイルなので、.NET 4.xに移行した方がよい。

ネイティブ実行可能なコードの生成

 プロファイルが決まると、AppDomainが生成されて、そこにアセンブリがロードされ、実行エンジンによってCPU命令に変換されたCILのコードが実行される。現在、実行エンジンは2種類ある。「mini」と呼ばれるJITコンパイラー(実行時コンパイル)とAOTコンパイラー(事前コンパイル)だ(以下、単にJITAOTと表記)。ちなみに以前はこの他に「mint」というインタープリターもあったが、すでに役目を終えて消滅した。AOTは、.NETで言うところのNGenが似たようなことをやっているが、アプリケーションの実行を伴うことなく、dllをターゲットCPUの命令にコンパイルして、ネイティブの共有ライブラリを生成してしまう機能だ。

 monoランタイムでは、自ら直接CPU命令を生成するmono自身のコード生成エンジンに加えて、LLVMによるコード生成エンジンもサポートしている。LLVMがより高速なコードを生成する部分は少なくない。一方、LLVMをロードするために必要なメモリが増加するので、LLVMは万人向けではない。

 monoのAOTは、実際には「フルAOT」と「通常のAOT」の2種類がある。AOTは、全てのCILコードをネイティブ命令に変換できるものではない。実行時にCPU命令をJITで生成することが不可欠な機能がある。例えばSystem.Reflection.Emit名前空間のAPIを使用して、動的にコードを生成して実行することは、AOTでは不可能だ。通常のAOTモードの場合、AOT変換されないコードに遭遇したら、コードはJITにフォールバックして実行される。フルAOTの場合はエラーとなって終わりである(次の画面はその例)。

AOT制約による強制終了を記録したiOS Log

 この世界には、動的に生成されたコードが動作しない環境がある。有名なところでは、JailbreakされていないiOS環境が挙げられる。iOSは、コードを実行するプラットフォームのレベルで、動的にコードを生成するために必要となるコードを無効化してしまう。フルAOTは、このようなプラットフォームでコードを実行するためにある。Xamarin.iOSは、ビルドツールチェーンで、自動的にこのAOTを使用してアプリケーションコードをビルドする(シミュレーター用ビルドを除く)。AOTの制約については、今後、Xamarin.iOSについての回であらためて言及するつもりだ。

ガベージコレクション(GC)

 monoランタイムは、長い間、古典的GCの代表的な存在である「Boehm GC」に手を加えたものを使用していたが、現在では世代別ガベージコレクションを実装している。この実装は「SGen」と呼ばれている(もともとは「Simple Generational GC」だったのだが、今は明らかにsimpleではない)。

 mono 3.2以降はデフォルトでSGenが有効になっている。Xamarin.iOS(厳密にはUnifiedプロジェクト)とXamarin.Androidでは、SGenが使用されている。

 また、これはMonoチームから時折表明される展望ではあるが、近くない将来に、.NET CoreのランタイムであるCoreCLRで使用されているGCをmonoに取り込む計画が実行されるかもしれない。

 GCの設計にはさまざまなアプローチがあって、「世代別」というのはSGenの1つの側面にすぎない。GCについて詳しく学ぶなら、『ガベージコレクションのアルゴリズムと実装』という書籍が勉強になる(筆者の中村氏は、Monoハッカーにより書かれたSGenについてのブログ記事の訳文も公開されている)。

mscorlibの内部呼び出し機能(およびファイルI/Oの特記事項)

 monoランタイムのもう1つの重要な機能は、mscorlib.dllの中でMethodImpl属性に「MethodImplOptions.InternalCall」を指定されて宣言されているexternメソッドの実体であるInternalCallだ(monoのソースコードでは「icall」と呼ばれている)。monoのmscorlibのソースコード(以下、「ソース」と表記)には、C#では実装できないためCでランタイム機能を呼び出す部分が不可避的に存在する。型システムへのアクセスやスレッドの操作、I/Oの操作などがあるので、これらはInternalCallを通じて、monoランタイムでネイティブに実行される。

 ここで特に言及しておきたいのがI/Oまわりだ。この部分は、Windows以外の環境では、「io-layer」と呼ばれるWindows I/O APIのエミュレーション実装が担当していて、直接にファイルI/Oが行われているわけではない。一般的には、WindowsとLinuxでは、ファイル処理に大きな違いがある。一番重要なのは、Linuxでは大文字と小文字が区別されることだ。Windows上では大文字と小文字が区別されないので、例えば以下のようなコードは問題なく動作する。

C#
File.WriteAllText ("FILE.txt", "test string");
return File.ReadAllText ("file.txt");
大文字と小文字を区別しないでファイル名を取り扱うコード例

 これをLinux環境に持っていくと、およそ失敗する。Linux上で使用されるファイルシステムの多くは、「FILE.txt」と「file.txt」を別物として扱うからだ。これはプログラマーが自ら注意すべき事柄だが、.NETアプリケーションは多くがWindows上で開発されているため、Windowsで動けばOKという前提で書かれているものが多い。そのようなプログラムでもある程度動かせるように、monoには、「MONO_IOMAP」という環境変数で、Windows I/Oのような挙動を指定できる機能がある。「MONO_IOMAP=all」と指定してmono上でアプリケーションを実行すると、上記のコードも想定通りに動作する(ちなみに、これを使えばファイル名の問題が全て解決するというわけではない。あくまでファイルI/O APIが影響できる範囲でのみだ。ファイル名の大文字・小文字は常に一致させておく方が望ましい)。

 ちなみにMac OS Xで使用するHFS+ファイルシステムでは、(デフォルトのフォーマットでは)大文字・小文字を区別しない。一方で、これはmonoに限らない話だが、HFS+はファイル名を扱う際にUnicode NFD正規化に類似する独自の変換を施すため、Mac OS Xとそれ以外の環境でクロス開発している場合には気を付けた方がよい(NTFSやext4などのファイルシステムは、特にこのような変換はしない)。iOSはMac OS Xを、AndroidはLinuxを、それぞれ基に構築されたOSである、ということを覚えておくと、わなにかからずに済むかもしれない。

クラスライブラリ

概要

 さて、monoランタイムについての説明がかなり長くなってしまったが、monoの最後の構成要素であるクラスライブラリについて、ざっくり説明したい。

 monoにどのクラスライブラリのアセンブリがあるかは、GitHubのリポジトリを見るのが手っ取り早いが、それぞれ説明するだけでも多すぎるので、概要だけを書いておきたい。基本的に、Xamarinがサポートしている分野は、商用サポートとして比較的手厚い改善を受けている。逆に、現在のXamarinがモバイルプラットフォームで使用していないクラスライブラリは、ほぼメンテナンスされていない状態だ。

 .NET 3.0で追加されたAPIは、大半が存在しない。WPF(Windows Presentation Foundation)とWF(Windows Workflow Foundation)が無く、一部のコードが「olive」という別のリポジトリに形だけ存在している。WCF(Windows Communication Foundation)も、モバイルで使われている部分以外はほぼ未完成で止まっている。モバイルプロファイルにおけるWCFは、Silverlight 2.0のものと、ほぼ同じだ。

 Windows固有のAPIを前提とするライブラリ(System.Management.dllSystem.Core.dllSystem.IO.Pipes名前空間など)も実装は無い。COMもWindows固有の機能なので、monoで動作することは期待できない(実のところ、Windows限定でmonoランタイムにCOM機能を実装していたコミュニティハッカーは存在したが、いずれにしろ依存するライブラリ側がWindows専用になるので、monoでサポートする意義はほぼ無い)。

 .NETのクラスライブラリは、あまりにも膨大であり、それを実装するMonoのクラスライブラリには、未実装のものも多い。具体的には、型やメンバーが存在しないものと、存在はしているが呼び出してみるとNotImplementedExceptionを投げるものがある(本来、どの型がどれだけ実装されているかという情報は、Monoの「class status」のwebページで確認できるようになっているのだが、本稿執筆時点で更新が止まった状態になっている)。

referencesourceとの関係

 ちなみに、.NET Frameworkのクラスライブラリがreferencesourceとしてオープンソース化され、その中にはWCFやWFを含め、サーバーサイドで利用できるさまざまなライブラリが含まれているが、それらはWindowsでのみ動作する既存の.NET Frameworkのソースであり、直ちにMonoに取り込めるものではない。また、Xamarinは動的コード生成が許されないiOS環境などでも動作するように実装されてきた経緯があり、マイクロソフトの実装が適切に動作するか検証し、場合によっては取り込みは断念しなければならない。

 筆者は自分がメンテナンスしていたXML関係のライブラリの大部分をreferencesourceの実装に切り替えたが、動的コード生成を前提とするXmlSerializerについては、従来のコードをそのまま使用している。同様に、System.Data.dllも、SQL Serverクライアントの実装がクロスプラットフォームになっていないため、その部分のみ従来のコードを使用している。筆者が実装していたWCFの置き換えも、一部を除いては、アセンブリ間の相互参照が強すぎて、大掛かりな差し替え作業が必要になり、現状では着手できていないのが(2016年前半の)現状だ。

 そうはいっても、referencesourceの取り込みは、特にmscorlib.dllSystem.dllSystem.Core.dllといったライブラリにおいて、だいぶアグレッシブに行われており(Mono 4.xの初期の頃は、それらに起因するリグレッションも少なからず存在していた)、現在では多くのコードがreferencesourceに置き換えられている。

クラスライブラリのソースの構成

 monoのクラスライブラリは、monoのソースツリー上はmcs/classディレクトリ以下に大量に作成されている(歴史的には、C#で書かれたコードは全て「mcs」というモジュールに含まれていた。GitHubに移行した際にmonoのソースツリーに統合された)。ディレクトリ構成のルールは基本的に次の通りだ。

   mcs/class/[Assembly]/[Namespaces]/[Type].cs

 また、クラスライブラリのテストにはNUnit(2013年の時点ではやや古く、2.4.8である)が用いられており、各ライブラリのテストのソースのディレクトリ構成は、おおむね次のようになっている。

   mcs/class/[Assembly]/Test/[Namespaces]/[Type]Test.cs

 ディレクトリ構造の主な例外として、mscorlibアセンブリ(mscorlib.dllファイル)は「corlib」、System.Windows.Formsアセンブリ(System.Windows.Forms.dllファイル)は「Managed.Windows.Forms」というディレクトリ名になっている(他にもいくつか例外はある)。クラスライブラリはもちろん全てC#で書かれている。ただし、ビルドにIDEは用いておらず、makeコマンドが使われている。ソースについては、aspnetwebstackentityframeworkrxcecilなど、外部のプロジェクトをgit submoduleで取り込んでいるものもあるが、アセンブリのディレクトリ構成は変わらない(ソースのリストはサブモジュールの中を参照している)。

 ソースのコーディング規則はmono独自のものだ。Linux kernelの文化に親和的で、不必要に行数を増やさない設計になっている。このコーディング規則が気に入らないがソースは読む必要がある、という場合は、MonoDevelopのコーディングスタイル設定をVisual Studioにした上で自動フォーマットを設定するとよい(MonoDevelopの使い方については回をあらためて論じる予定だ)。

 もしクラスライブラリの未実装部分を実装したり、バグなどを発見して修正を試みたりなどしたい場合は、一度、monoのツリー全体をビルドした後、そのアセンブリのディレクトリに移動して、make run-testを実行すると、NUnitテストが実行されるので便利だ(次の画面はその例)。ちなみにビルドされた.dllファイルなどは「mcs/class/lib/net_4_5」のようなディレクトリに生成される。

make run-testの実行結果

 monoのクラスライブラリには、実際には複数のプロファイルがある。.NET 2.0、.NET 4.0、.NET 4.5とあって、それぞれ.NETのプロファイルに相当する。クラスライブラリのディレクトリでmakeを実行したときにビルドされるのは、最新のプロファイルだけだ(現在は.NET 4.5)。古いプロファイルのアセンブリをビルドするためには、make PROFILE=net_2_0といったビルドを実行する必要がある。実はさらにモバイルプロファイルもあるのだが、それについては次回論じることとしたい。

 もしクラスライブラリをハックできたら、差分はGitHubでpull requestとして送ってもらえれば、きっと歓迎されてレビューされるだろう(数が多いため、反応がないこともしばしばあるので、メーリングリストなどでコメントを求めるなどの合わせ技も有効だ)。筆者はバージョン0.18のころからMonoの開発に参加しているが、これまでも日本から10人前後のパッチコントリビューターがいた。日本からさらに多くのコードヒーローが参加してもらえるなら、これほどうれしいことはなかなかない。

※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

1. Xamarinを構成するソフトウェア。その主要な10要素とは?

Xamarinは何を提供しているのか? その主要なソフトウェア構成要素として、Mono、Gtk#、MonoDevelopとXamarin Studio、Xamarin.iOS、Xamarin.Android、Xamarin.Mac、Visual Studioアドイン、Xamarin.Forms、Xamarinコンポーネント、Xamarin Test Cloudなどについて紹介。

2. Xamarinの基盤となっている「Mono」と、C#コンパイラー「mcs」

Xamarinにおけるソフトウェアの基盤であるMonoを深く理解すれば、Xamarin製品の理解はもっと深まる。今回はMonoの成り立ちから、そのソフトウェア構成、C#コンパイラーの内容までを解説する。

3. 【現在、表示中】≫ Xamarinの基盤「Mono」のmonoランタイムとクラスライブラリ

Xamarinにおけるソフトウェアの基盤であるMonoを深く理解すれば、Xamarin製品の理解はもっと深まる。今回はmonoランタイムと、Monoのクラスライブラリについて解説する。

4. Monoのモバイル化の流れ ― Xamarin.iOS/Xamarin.Androidの誕生

デスクトップ環境での動作を主眼に開発された「.NET」のオープンソース実装である「Mono」は、どのようにモバイル開発に向かって流れていくことになったのか。

5. Xamarin.iOSの仕組みとアプリケーションの構成

いよいよXamarin.iOSを取り上げる。その仕組みや、Xamarin.iOSアプリの作成/ビルド/実行とデバッグなどについて解説。

サイトからのお知らせ

Twitterでつぶやこう!