(なんかこのネタ、誰かがやってそうな気がしつつ)
一部ではバージョンがわかりづらいなどとも言われている .NET Core の起動の仕組みについて、適宜ソースコードを眺めつつ、整理したいと思います。私は中の人ではなく、コードを解析しながら書いているので、おかしな点がありましたら twitter 等で教えていただけると嬉しいです。
構造
ご存知のように、.NET Core は、コンパイル結果の実行可能アプリケーションまたは dotnet コマンドから実行します。
その時の動作の流れを図にすると、以下のようになっています。
この図において、長方形はファイル、角丸四角形はレイヤー、青の長方形は説明のためのグルーピングです。
この図の要素を簡単に説明すると、以下のようになります。
- dotnet コマンド。別名 .NET CLI と言われているのはこれ(のはず)です。
muxerと呼ばれることもあります。実体としては、以下の 2 つになります。
dotnet。Windows の場合はdotnet.exeになります。これは後述するアプリケーションランチャーです。dotnet.dll。.NET CLI の本体です。.NET CLI のリポジトリにあるソースコードをビルドすると生成されます。
- app。特定のランタイム向けに
dotnet publishすると生成される実行可能ファイルです。 - corehost。Windows の場合は
corehost.exeとなります。コマンドラインプログラムとしての .NET Core のランチャーです。 - hostfxr。Windows の場合は
hostfxr.dll、Mac OS X の場合はlibhostfxr.dylib、Linux の場合はlibhostfxr.so。corehostの実体になります。 - hostpolicy。Windows の場合は
hostpolicy.dll、Mac OS X の場合はlibhostpolicy.dylib、Linux の場合はlibhostpolicy.so。CoreCLR ホスティング API に渡すための各種情報を初期化します。まさに、「ホスティングポリシー」です。 - Hosting API。CLR の Hosting API の CoreCLR 版。COM に準拠した CLR の Hosting API に対して、CoreCLR の Hosting API は UNIX の関数に近い形になっています。
ICLRRuntimeHost2のラッパーとしての C 言語の関数です。 - ICLRRuntimeHost2。CoreCLR の Hosting API の実体です。ちなみに、CLR の Hosting API は
ICLRRuntimeHostインターフェイスです。
これらは、ホスティング API(ICLRRuntimeHost2 とラッパーである Hosting API)と、CoreHost(corehost、hostfxr、hostpolicy に加え、この後説明する dotnet も含まれます)に大別されます。
ホスティング API
ホスティング API は、その名の通り CoreCLR(.NET Core のランタイム)をプロセスにロードして、実行するための API です。.NET Core のプログラムを実行する場合、このホスティング API を何らかの形で実行する必要があります。
.NET Core の場合、以下の 4 つの関数が定義されています。ここで、文字列は、Windows の場合は ANSI、それ以外の場合は UTF-8 で渡します。
// CoreCLR を初期化。戻り値は HRESULT。
int coreclr_initialize(
const char* exePath, // 実行する exe のパス。
const char* appDomainFriendlyName, // 初期 AP ドメインの名前。
int propertyCount, // CoreCLR プロパティの数。つまり、propertyKeys と propertyValues の要素数。
const char** propertyKeys, // CoreCLR プロパティキー文字列の配列。
const char** propertyValues, // CoreCLR プロパティキー値の配列。
void** hostHandle, // ホストへのハンドルが格納される(実体は ICorHost2)
unsigned int* domainId // 初期 AP ドメインの ID が格納される。
);
// CoreCLR のシャットダウン。戻り値は HRESULT。
int coreclr_shutdown(
void* hostHandle, // coreclr_initialize で返されたホストハンドル。
unsigned int domainId // アンロードする AP ドメインの ID。
);
// 任意の静的メソッドへのデリゲートを作成し、それを呼び出すための関数ポインターを取得する。
int coreclr_create_delegate(
void* hostHandle, // coreclr_initialize で返されたホストハンドル。
unsigned int domainId, // 対象のメソッド(を定義する型)がロードされている AP ドメインの ID。
const char* entryPointAssemblyName, // 対象のアセンブリの名前。
const char* entryPointTypeName, // 対象の型の名前。
const char* entryPointMethodName, // 対象のメソッドの名前。
void** delegate // 関数ポインターが格納される。
);
// アセンブリのメインエントリポイント(Main)を実行。戻り値は HRESULT。
int coreclr_execute_assembly(
void* hostHandle, // coreclr_initialize で返されたホストハンドル。
unsigned int domainId, // 対象のメソッド(を定義する型)がロードされている AP ドメインの ID。デフォルトドメインでなければなりません。
int argc, // argv の要素数
const char** argv, // Main メソッドに渡す引数の配列。
const char* managedAssemblyPath, // Main を定義するアセンブリへのファイルパス。
unsigned int* exitCode // Main の戻り値。`void` だった場合は `0`。
);
ホスティング API の動き
ホスティング API の実装は、 C 関数版である unixinterface.cpp と、
そこから呼び出す ICLRRuntimeHost2 COM インターフェイスの実装である CorHost2 クラスです。
使い方については、ホスティング API のドキュメント を参照しつつ、以下のソースコードを参照するといいでしょう。
- corerun。CoreCLR に付属する、テスト用の簡易コマンドラインランチャーです。これを使うことで、.NET CLI の開発が追い付いていない状態でも coreclr の開発版を実行できます。
- Azure IoT Gateway SDK .NET Core Binding。IoT のシステムで、よりセンサーに近いモノ側に配置するプログラム用(いわゆるエッジコンピューティング用)のフレームワークです。.NET Core 以外からのホスティング API の使用例として参考になるでしょう。設計ドキュメントもあるので、こちらの方がわかりやすいかもしれません。
- 余談ですが、Node.js や Java や CLR をロードするサンプルプログラムとしても面白いかと。
ホスティング API の動きを簡単に説明すると、以下のようになります。
coreclr_initialize
- PAL(Platform Abstraction Layer)を初期化します。
ICLRRuntimeHost2::SetStartupFlags()で初期化フラグを設定します(具体的には、*.runtimeconfig.jsonにあるSystem.GC.ConcurrentSystem.GC.Server、System.GC.RetainVM)ICLRRuntimeHost2::Start()でランタイムを起動します。
- 内部的には、ランタイムの起動カウントをインクリメントし、
InitializeEE()を呼び出します。 - なお、シャットダウン済みの場合は単に失敗します。
- 内部的には、ランタイムの起動カウントをインクリメントし、
ICLRRuntimeHost2::CreateAppDomainWithManager()を使用して、以下のように AP ドメインを初期化します。
APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPSを指定して、Any CPU でないアセンブリの実行を許可します。APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROPを指定して、P/Invoke と COM Interop を許可します。APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPSを指定して、コードアクセスセキュリティを無効化します。- AP ドメインマネージャーは指定しません。
coreclr_shutdown
ICLRRuntimeHost2::UnloadAppDomain()で AP ドメインをアンロードします。ICLRRuntimeHost2::Stop()でランタイムを停止します。
- 内部的には、ランタイムの起動カウントをデクリメントし、
0になったらシャットダウン済みフラグを設定します。
- 内部的には、ランタイムの起動カウントをデクリメントし、
- PAL をシャットダウンします。
coreclr_create_delegate
ICLRRuntimeHost2::CreateDelegate()を呼び出します。
- 呼び出すメソッドには以下の制限があります。
- 静的でなければなりません。
- ジェネリックメソッドであってはなりません。
[AllowReversePInvokeCallsAttribute]で修飾されていなければなりません。- メソッドはオーバーロードされていてはいけません。
coreclr_execute_assembly
ICLRRuntimeHost2::ExecuteAssembly()を呼び出します。
Assembly::ExecuteMainMethod()を呼び出します。- メタデータからエントリポイントを探し、現在のスレッドをフォアグラウンドスレッドにし、エントリポイントを実行します。
CoreHost
さて、ホスティング API は API なので、シェルから直接実行するには、何らかの CLI プログラムが必要になります。それがここで紹介する CoreHost です(正式名称ではない気がしますが、そこは目を瞑ってください)。なお、「dotnet は?」とか「publish すると実行可能ファイルが作れるじゃん」とかいう疑問はちょっとわきに置いておいてください。この後ちゃんと説明します。
CoreHost はホスティング API を呼び出す「コマンドラインランチャー」であり、「マルチプレクサー(muxer)」です……意味が分かりませんね。基本的に、このコマンドは corehost <アプリケーション.dll> <引数> という形式でアプリケーションを実行するためのものです(まさに「ランチャー」です)。
そして、以下のように、corehost の実際の動作は、状況に応じて変わります。
<自ファイル名>.dllファイルがあり、かつ以下の条件を満たす場合、「スタンドアローンモード」になります。
<自ファイル名>.deps.jsonが存在する<自ファイル名>.deps.jsonも<自ファイル名>.runtimeconfig.jsonも存在しない
- そうではなく、同じディレクトリに
coreclr.dll(またはlibcoreclr.soかlibcoreclr.dylib)がある場合、「分離 FX モード」になります。 - そうでない場合、Muxer モードになります。
この後、各モードの詳細について見ていきますが、もう一つ説明しておくことがあります。それは、corehost はそのままではなく リネームして使用 されるということです。
具体的には、以下のようにリネームされて使用されます。
<アプリケーション>[.exe]。dotnet publishした結果生成される実行可能アプリケーション。dotnet[.exe]。SDK のルートディレクトリに配置される、.NET CLI と呼ばれるものです。
つまり、これまで実行可能アプリケーションや dotnet コマンドだと思っていたものの実体は、単一の corehost だったということです。
それでは、それぞれのモードについて、その使われ方とともに詳細に見ていきましょう。
各モードについて
CoreHost は、前述のように、自分自身のファイル名がどうなっているかと、同じディレクトリにどのようなファイルがあるかによって 3 つのモードがあり、それぞれ動作が変わります。
分離 FX モード
これがおそらく最も基本的なモードです。コード上は split_fx というシンボルになっています。
以下のように、引数で指定された .NET Core のプログラム(マネージドアセンブリ)を実行するモードです。
dotnet <アプリケーション>.dll から実行するときのモードでもあります。
- 第1引数で指定されたマネージドアセンブリを実行します。
- 引数として、
--depsfileと--runtimeconfigをサポートします。
つまり、dotnet app.dll を実行すると、以下のように動作します。
dotnet.exeという名前のcorehost.exeが、分離 FX モードでapp.dllを実行します。dotnet.exeはホスティング API を使用して、app.dllに定義されたエントリポイントを呼び出します。
スタンドアローンモード
これは最も基本的ではないモードなのですが、3 番目のモードが複雑なので先に説明します。
これはあたかも自分自身が .NET Core アプリケーションのように動作するモードで、コンパイルした <アプリケーション>.exe を実行するときのモードでもあります。
<自ファイル名>.dllという名前のマネージドアセンブリを実行します。- 引数として、
--depsfileと--runtimeconfigをサポートします。
つまり、dotnet publish で生成される app.exe を実行すると、以下のように動作します。
app.exeという名前のcorehost.exeが、スタンドアローンモードでapp.dllを実行します。app.exeはホスティング API を使用して、app.dllに定義されたエントリポイントを呼び出します。
Muxer(マルチプレクサー)モード
これは、2 番目に基本的で、おそらく最も目にすることが多いモード、.NET CLI 用のモードです。
- 第 1 引数が
execというコマンドが実行された場合、第 2 引数のマネージドアセンブリを実行します。つまり、分離 FX モードとして動作します(exec モード)。 - 引数として、
--fx-versionと--roll-forward-on-no-candidate-fxをサポートします。 - そうでない場合、(
--fx-versionで指定された)coreclr 本体と同じディレクトリにあるdotnet.dllを実行します。 - これは、
dotnetコマンドで CLI を使用するときのモードです。
- スタンドアローンモードで
dotnetを実行する場合、CoreCLR 本体とdotnet.dllがdotnet[.exe]同じディレクトリに存在する必要があり、バージョン切り替えができなくなるのでこのようになっているのでしょう。
- スタンドアローンモードで
つまり、dotnet run app a b c を実行すると、以下のように動作します(説明を簡単にするため、アプリケーションはビルド済みとします)。
dotnet.exeが Muxer モードで動作し、runに拡張子がないため、dotnet.dllを呼び出します。dotnet.dllは組み込みコマンドrunを認識し、RunCommandオブジェクトを初期化します。RunCommandはプロジェクトファイルを msbuild の API でロードしします。- msbuild は、
RunProgramプロパティとしてdotnetを、RunArgumentプロパティとしてexec path/to/bin/app.dll a b cを設定します。
- 具体的には .NET SDK に含まれる
Microsoft.NET.Sdk.targetsで設定されます。これは、菊池さんのブログにあるように、プロジェクトの.csprojから暗黙的に参照されます。
- 具体的には .NET SDK に含まれる
RunCommandRunProgramプロパティとRunArgumentプロパティからコマンドラインを生成します。
- つまり、
dotnetとexec path/to/bin/app.dll a b cを生成します。
- つまり、
- 外部プロセスとして、
dotnet exec path/to/bin/app.dll a b cを実行します。 dotnet(corehost)が exec モードの Muxer モードとして、ホスティング API を使用して、path/to/bin/app.dllに定義されたエントリポイントを呼び出します。
補足:CoreHost とアセンブリプロービング
.NET Core におけるアセンブリのプロービングは CLR のものとは異なります。
具体的には、ホスティング API で指定するプロパティによってその動作が変わります。
通常、ホスティング API を呼び出すのは、dotnet(corehost)になるため、プロービングの動作は、corehost の実装に依存することになります。
ざっくりいうと deps.json にない値はカレントディレクトリからロードしません。
具体的に言うと、Assembly.Load などで動的にロードする分には問題ないのですが、そのロードしたアセンブリが参照するアセンブリはロードできません。
詳細についてはこちらを参照してください。
プロービングの詳細については、今回は説明しません(また機会があれば……)。
ただ、プロービングの動作がホストプログラムに依存すること、dotnet(corehost)の仕様は coreclr ではなく cli のドキュメントとして存在すること、
そして具体的な動作を調査するための corehost のコードは(なぜか)インストーラー(core-setup)のリポジトリにあることを覚えておいてください。
ランチャーの動作から言えること
これまでの説明をもとに、いくつかの動作が説明できます。
.NET Core 実行可能アプリケーションとは
スタンドアローンモードで説明したように、<アプリケーション名>.exe(Windows 以外では <アプリケーション名>)という名前にリネームされ、スタンドアローンモードで実行される corehost です。
(リネーム処理は .NET Core SDK の Microsoft.NET.Publish.targets ファイルに書かれています)
.NET CLI とは
Muxer モードで動作する corehost.exe と dotnet.dll に実装されたコマンドフレームワーク(と組み込みコマンド)です。
実行可能ファイルが RID(ランタイム ID)を指定した publish でしか生成されない理由
corehost は配布先のプラットフォーム依存だからですね。
dotnet と dotnet help の違い
しばやんさんのブログでの .NET Core のバージョンが難しいの中で出た、CLI のバージョン番号がよくわからないという話も、これまでの説明にちょっと引数パースの仕様を加えることで、説明できます。
dotnetを実行した場合、corehostの必須引数が足りないため、corehostのヘルプが表示されます。そのため、CoreCLR(のランチャーであるhostfxr)のバージョンが表示されます。dotnet helpを実行した場合、corehost dotnet.dll help扱いになり、CLI のHelpCommandが実行されます。そのため、.NET CLI のバージョンが表示されます。
まとめ
- .NET Core プログラムを実行するということは、Hosting API を実行するということ。
- 通常、Hosting API は
corehost[.exe]から実行する。 dotnet[.exe]コマンドはリネームされたcorehost[.exe]である。- 実行可能アプリケーションもリネームされた
corehost[.exe]である。 corehostはその配置場所に応じて動作を変える。これによって、.NET CLI としての動作や、汎用ランチャーとしての動作、実行可能アプリケーションとしての動作を使い分けている。
参考文献
- How the dotnet CLI tooling runs your code 上記の内容の確信を得るのにお世話になりました(というか最初にこれを見ればよかった)
- Hosting .NET Core .NET Core の Hosting API のチュートリアル
- corehost のソースコード なぜか core-setup リポジトリにあります。
- corehost のプロービングの仕様 アセンブリのプロービングのトラブルシュートはこちら
- CLI のソースコード ここに C/C++ のコードはないというね
- CoreCLR のホスティング API のコード 相変わらずの mscoree
- CoreCLR のホスティング API のヘッダーと corerun のコード ここに corehost もあると思うじゃないですか……
- CoreCLR のホスティング API の実装 これに限らず、vm ディレクトリにだいたいの実装があります
- Azure IoT Gateway SDK .NET Core Binding Hosting API の活用例として