こちらのAdventCalendarの2日目の記事です。
はじめに
先日.NET6が正式リリースされました!
目につくものと言えばやはり、C#10や、SourceGeneratorなどがありますね!
「.NETのランタイムそのものがどのような仕組みで動いているか」なんてことは誰も気に留めません。
それはミドルウェアとしてとても理想的なことです。
しかし、エンジニアたるもの、やっぱり中身がどうなっているのか気になりませんか?
ということで、.NETアプリケーションを起動した瞬間から、エントリポイントが呼ばれるまでをトレースしてみましょう!
.NETCore時代の解説にはなりますが、.NETCoreの起動までを概念的に解説した良い記事があります。
本記事では、概念的な部分よりも、コード、実装にフォーカスていきます。
なので、その前に読んでおくと、イメージがしやすくなると思います。
相手は.NETのランタイム、そこそこ長くなってしまいましたが、安眠導入にお使いください。
対象・条件
- .NETについてユーザーとしての知識はある程度ある方向けです(C#アプリが難なく書ける程度)
-
dotnet publish -r win-x64 --self-contained=false
によって出力されたExeを実行した場合とします -
wmain
からProgram.Main
を叩くまでのNativeコードを対象とします - 厳密には、.NETランタイムというより、.NETランタイムホストが大部分を占めます
- JIT、GC、AppContextなどについては割愛します
- バージョンは、
dotnet/runtime
のv6.0.0
を対象とします - 例外的処理や通過しない処理、重要度の低い処理など、本筋に関係ない部分は注釈なく省略・改変しています
- コード中のコメントについて、英語は引用元、日本語は私の書いたもので区別しています
用語
- Native/Unmanaged: C++で書かれていているコード、それをコンパイルしたモジュール(C++/CLIは今回考慮しません)
- Managed: C#などの.NET言語で書かれているコード、それをコンパイルしたアセンブリ
- モジュール: システム上で直接実行可能な、Exe、Dllの
- アセンブリ: .NET上で実行可能なManagedなコードをコンパイルしたDll
- システム: .NETランタイムより下で動いている系(基本的にはOSのこと)
- Managedエントリ: C#で言う
static void Program.Main(string[])
のこと(エントリポイントの変更は考慮しない) - ランタイム(狭義): sharedディレクトリ下の種類ディレクトリ下のバージョンごとに切られたディレクトリ無いのファイル群をランタイムと呼ぶことにします(コード上ではFramework,FXとも表現されています)
- ランタイム(広義): dotnet/runtime 全般
- corehost: 公式のドキュメント上では、
apphost
(comhost
,ijwhost
, Entry-point Hosts)とも呼ばれています;コード上ではcorehostなので、corehostと呼ぶことにします
起動までの呼び出し履歴
まず全体像を掴むために、Program.Main
までの呼出し履歴を見てみます。
「プロジェクトプロパティ>デバッグ>ネイティブコードをデバッグを有効にする」にチェックを入れることで、Nativeな呼出し履歴もすべて見ることができます。
これは、空のSolutionからC#のConsoleアプリプロジェクトを作っただけでできます。
.NETランタイムのモジュールも一緒にデバッグしたい場合、以下のようにすることでできます。
言語 | モジュール | 関数 |
---|---|---|
C# | ConsoleApp1.dll | ConsoleApp1.Program.Main |
C++ | hostpolicy.dll | coreclr_t::execute_assembly |
C++ | hostpolicy.dll | run_app_for_context |
C++ | hostpolicy.dll | run_app |
C++ | hostpolicy.dll | corehost_main |
C++ | hostfxr.dll | execute_app |
C++ | hostfxr.dll | ::read_config_and_execute |
C++ | hostfxr.dll | fx_muxer_t::handle_exec_host_command |
C++ | hostfxr.dll | fx_muxer_t::execute |
C++ | hostfxr.dll | hostfxr_main_startupinfo |
C++ | ConsoleApp1.exe | exe_start |
C++ | ConsoleApp1.exe | wmain |
C++ | ConsoleApp1.exe | invoke_main() |
C++ | ConsoleApp1.exe | __scrt_common_main_seh() |
kernel32.dll | @BaseThreadInitThunk@12() |
|
ntdll.dll | __RtlUserThreadStart() |
|
ntdll.dll | __RtlUserThreadStart@8() |
まず、下から順番に見ていくと、謎の関数をいくつか経た後に、wmain
が呼び出されています。この謎の関数については.NETというよりは、Windowsの仕組みの話になりますので割愛します。
VC++でWindowsアプリを作った事のある方ならおなじみかもしれませんが、wmain
はmain
のWide文字バージョンです。
ここではWide文字とUnicode文字の差異は重要ではないので、main
だと思って大丈夫です。つまり、ここが.NETのランタイムにとってのエントリポイントとなります。
その後、Nativeであるhostfxr.dll
、hostopolicy.dll
、そしてManagedであるConsoleApp1.dll
と3つのDllが確認できます。
.NETのランタイムは、Managedな領域の管理やコールバック、GC、JITなどとの橋渡しをする本体部分(coreclr.dll
)と、それを立ち上げるまでのホスト部分(ConsoleApp1.exe
、hostfxr.dll
、hostopolicy.dll
)に分かれています。
ここでこの4つのNativeモジュールについて、どんなものなのかを説明します。
各Nativeモジュール
以下に抽象的な説明があります。(特にこれに沿っているわけではないですが)
ConsoleApp1.exe
(corehost
)
ConsoleApp1というのは、dotnet publish
したときのプロジェクト名によって変わりますが、デフォルト?というかこのモジュールコンパイル時の名前はcorehost
です。
以降はcorehost
と呼ぶことにします。「corehost
がすべての始まりである」ということです。
話が.NETアプリのビルド時のことに逸れますが、--self-contained
を有効にしても、このモジュール含めてNativeのモジュールはコンパイルされません。
hostfxr.dll
、hostopolicy.dll
、coreclr.dll
は、そもそもシステムにインストールされていることが前提なので出力しません。(--self-contained
では、SDKからコピーされるだけです)
corehost
はSDKに含まれているものをコピーして、Managedプロジェクト名にリネームして出力します。(もう一工夫ありますが、後述)
更に話が逸れますが、普段使っているdotnet
コマンドですが、このdotnet.exe
もまたcorehost
のリネームです。
corehost
には色々なモードがありますが、基本的に
- アプリケーションのエントリポイント(
wmain
をエクスポート) -
hostfxr.dll
を見つける -
hostfxr.dll
の関数を呼び出す
といった役割を担います。
エントリポイントからコードを追っていきましょう!
以下のコードでは、host_path
,app_path
,app_root
を取得し、hostfxr
を解決しています。
host_path
,app_path
,app_root
は、脈拍と受け継がれ深い部分でも使われるので、何を意味しているかを覚えておいてください。
int __cdecl wmain(const int argc, const pal::char_t* argv[])
{
int exit_code = exe_start(argc, argv);
return exit_code;
}
int exe_start(const int argc, const pal::char_t* argv[])
{
pal::string_t host_path = /* corehostのパス; GetModuleFileNameW; (e.g. "./ConsoleApp1.exe") */ ;
pal::string_t app_path; // Managedエントリのアセンブリのパス (e.g. "./ConsoleApp1.dll")
pal::string_t app_root; // Managedエントリのアセンブリのパス (e.g. "./")
pal::string_t embedded_app_name; // corehostに埋め込まれた、Managedエントリのアセンブリの相対パス(e.g. "./ConsoleApp.dll")
// C#で言うTryGet操作、Exeに埋め込まれたManagedエントリの名前を抽出します(後述)
if (!is_exe_enabled_for_execution(&embedded_app_name))
{
trace::error(_X("A fatal error was encountered. This executable was not bound to load a managed DLL."));
return StatusCode::AppHostExeNotBoundFailure;
}
app_path.assign(get_directory(host_path));
append_path(&app_path, embedded_app_name.c_str());
app_root.assign(get_directory(app_path));
hostfxr_resolver_t fxr{app_root}; // → hostfxr_resolver_t::hostfxr_resolver_t
auto hostfxr_main_startupinfo = fxr.resolve_main_startupinfo(); // Dllエクスポート関数 hostfxr.dll!hostfxr_main_startupinfo を取得
const pal::char_t* host_path_cstr = host_path.c_str();
const pal::char_t* dotnet_root_cstr = fxr.dotnet_root().empty() ? nullptr : fxr.dotnet_root().c_str(); // .NETランタイムのインストールパス
const pal::char_t* app_path_cstr = app_path.empty() ? nullptr : app_path.c_str();
rc = hostfxr_main_startupinfo(argc, argv, host_path_cstr, dotnet_root_cstr, app_path_cstr);
return rc;
}
is_exe_enabled_for_execution
でManagedエントリを含むアセンブリを解決する
is_exe_enabled_for_execution
は初見で何しているか解り辛いですが、以下のようなことをしています。
一見するとセキュリティチェックのようにも見えてしまうのですが、セキュリティは一切関係ありません。結果だけを見れば、単にManagedアセンブリの相対パスを取得しているにすぎません。
まずcorehostをコンパイルする際には、Managedアセンブリはまだありませんので以下のようなデータをPEのイメージの.data
領域に埋め込んでおきます。
"c3ab8ff13720e8ad9047dd39466b3c89" // A
"74e592c2fa383d4a3960714caef0c4f2" // B
"c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" // AB
dotnet publish
する際にcorehost
を<AppName>.exe
にリネームして出力しますが、この時に、HostWriter
でABの領域にManagedアセンブリの相対パスを上書きします。
ABの文字列を検索したとき、A, Bについては仮に連続配置されていてもA+BにはならないのでA, BはそのままABだけ上書きされます。(A, Bは実際には"c3a...c89\0", "74e...4f2\0"となっているため)
そして実行時には、まずABを取得し、A+Bと比較します。もしA+B == AB
であればリネーム処理が正しくされていないということでエラーにします。
public static class HostWriter
{
private const string AppBinaryPathPlaceholder = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2";
private static readonly byte[] AppBinaryPathPlaceholderSearchValue = Encoding.UTF8.GetBytes(AppBinaryPathPlaceholder);
public static void CreateAppHost(
string appHostSourceFilePath, string appHostDestinationFilePath, string appBinaryFilePath,
bool windowsGraphicalUserInterface = false, string assemblyToCopyResorcesFrom = null, bool enableMacOSCodeSign = false)
{
using (FileStream appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true))
using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite))
{
// c3ab8...0c4f2 を、appBinaryFilePath(e.g. "./ConsoleApp1.dll")でReplaceする
var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath);
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
using FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create);
BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, appHostSourceStream.Length);
}
}
}
#define EMBED_HASH_HI_PART_UTF8 "c3ab8ff13720e8ad9047dd39466b3c89" // SHA-256 of "foobar" in UTF-8
#define EMBED_HASH_LO_PART_UTF8 "74e592c2fa383d4a3960714caef0c4f2"
#define EMBED_HASH_FULL_UTF8 (EMBED_HASH_HI_PART_UTF8 EMBED_HASH_LO_PART_UTF8) // NUL terminated
bool is_exe_enabled_for_execution(/* OUT */ pal::string_t* app_dll)
{
constexpr int EMBED_SZ = sizeof(EMBED_HASH_FULL_UTF8) / sizeof(EMBED_HASH_FULL_UTF8[0]);
constexpr int EMBED_MAX = (EMBED_SZ > 1025 ? EMBED_SZ : 1025); // 1024 DLL name length, 1 NUL
// "c3ab...3c89 74e59...0c4f2 \0" となっているので、HostWriterで引っかかる
// つまり、コンパイル時は、embed:="c3ab8...0c4f2 \0" だけど、実行時には書き換えられているとが期待される(e.g. "./ConsoleApp1.dll")
static char embed[EMBED_MAX] = EMBED_HASH_FULL_UTF8;
// この二つが仮に、モジュール内のバイナリに連続して配置されたとしても、
// "c3ab...3c89 \0 74e59...0c4f2 \0" となるので、間に挟まる "\0" によってHostWriterで引っかからない
// つまり、コンパイル時も実行時も同じ hi_part:="c3ab...3c89 \0", lo_part:="74e59...0c4f2 \0" で変わらない
static const char hi_part[] = EMBED_HASH_HI_PART_UTF8;
static const char lo_part[] = EMBED_HASH_LO_PART_UTF8;
// app_dll に embed をアサインして...
// hi_len + lo_len == embed が真なら、HostWriterが書き換えていないということなのでエラーにするチェックをする
}
hostfxr_resolver_t
でhostfxr.dll
を解決する
hostfxr_resolver
はコンパイルモードによってファイルが切り替わるので注意が必要です。
hostfxr_resolver_t
はhostfxr.dll
の場所を解決します。
ここで探しているのは、ランタイムの本体であるcoreclr.dll
ではなく、hostfxr.dll
であり、「ランタイムのリゾルバのリゾルブ」という2段構えの1段目の処理です。
検索の優先順位は
- Exeのあるディレクトリ内
- 環境変数 "DOTNET_ROOT_x64"
- 環境変数 "DOTNET_ROOT"
- レジストリ"[HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\x64]InstallLocation"
- ディレクトリ "C:\ProgramFiles\dotnet"
となっています。
hostfxr_resolver_t::hostfxr_resolver_t(const pal::string_t& app_root)
{
if (!fxr_resolver::try_get_path(app_root, &m_dotnet_root, &m_fxr_path))
{
m_status_code = StatusCode::CoreHostLibMissingFailure;
return;
}
if (!pal::load_library(&m_fxr_path, &m_hostfxr_dll)) // LoadLibraryExW で、hostfxr.dllをプロセスにロード
{
m_status_code = StatusCode::CoreHostLibLoadFailure;
return;
}
m_status_code = StatusCode::Success;
}
bool fxr_resolver::try_get_path(const pal::string_t& root_path, pal::string_t* out_dotnet_root, pal::string_t* out_fxr_path)
{
// "<root_path>/hostfxr.dll" が存在すれば、それを採用(root_pathは、Exeのあるディレクトリ)
if (root_path.length() > 0 && library_exists_in_dir(root_path, LIBFXR_NAME, out_fxr_path))
{
out_dotnet_root->assign(root_path);
return true;
}
pal::string_t default_install_location;
pal::string_t dotnet_root_env_var_name;
// .NETのランタイムがインストールされているディレクトリ(dotnet_root)をシステムに問い合わせる
//
// 1. 環境変数 "DOTNET_ROOT_x64" が設定されていれば、そこを起点とする
// 2. 環境変数 "DOTNET_ROOT" が設定されていれば、そこを起点とする
// 3. または、レジストリ"[HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\x64]InstallLocation" が設定されていれば、そこを起点とする
// 4. ディレクトリ "C:\ProgramFiles\dotnet" が存在すれば、そこを起点とする
if (get_dotnet_root_from_env(&dotnet_root_env_var_name, out_dotnet_root)) {}
else if(pal::get_dotnet_self_registered_dir(&default_install_location) out_dotnet_root->assign(default_install_location);
else if(pal::get_default_installation_dir(&default_install_location)) out_dotnet_root->assign(default_install_location);
pal::string_t fxr_dir = /* "<out_dotnet_root>/host/fxr" */ ;
return get_latest_fxr(std::move(fxr_dir), out_fxr_path); // "<fxr_dir>/<一番大きいい数字(最新)>/hostfxr.dll" を探す
}
hostfxr_main_startupinfo_fn hostfxr_resolver_t::resolve_main_startupinfo()
{
return reinterpret_cast<hostfxr_main_startupinfo_fn>(pal::get_symbol(m_hostfxr_dll, "hostfxr_main_startupinfo"));
}
hostfxr.dll
このモジュールは、.NETのランタイム本体を解決するためのモジュールです。
ここで、システムにインストールされた.NETランタイムの構成を確認しておきましょう。
以下は、.NET5と.NET6をインストールしている場合を想定しています。
C:\Program Files\dotnet
├ (省略)
├ host\fxr
| ├ 5.0.0\hostfxr.dll
| └ 6.0.0\hostfxr.dll
└ shared
├ (省略)
├ Microsoft.NETCore.App
| ├ 5.0.0
| | ├ (省略)
| | ├ System.Core.dll
| | ├ clrjit.dll
| | ├ hostpolicy.dll
| | └ coreclr.dll
| └ 6.0.0
| | ├ (省略)
| | ├ System.Core.dll
| | ├ clrjit.dll
| | ├ hostpolicy.dll
| | └ coreclr.dll
└ Microsoft.WindowsDesktop.App
├ 5.0.0
| ├ (省略)
| └ System.Drawing.dll
└ 6.0.0
├ (省略)
└ System.Drawing.dll
このように、hostfxr.dll
と、それ以外のランタイム本体は別でインストールされます。
hostfxr.dll
は複数インストールされていても、最新のものしか使われません。
また、ランタイム本体の方は、ランタイムの種類ごとに、その中でバージョンごとに分けてインストールされています。
それでは、このhostfxr
の実装を追っていきます。
まずは、corehost
からhostfxr_main_startupinfo
関数がGetProcAddress
によって呼び出さされます
SHARED_API int HOSTFXR_CALLTYPE hostfxr_main_startupinfo(const int argc, const pal::char_t* argv[], const pal::char_t* host_path, const pal::char_t* dotnet_root, const pal::char_t* app_path)
{
if (host_path == nullptr || dotnet_root == nullptr || app_path == nullptr) return StatusCode::InvalidArgFailure;
host_startup_info_t startup_info(host_path, dotnet_root, app_path); // host_startup_info_t::host_startup_info_t はメンバへのコピーのみ
return fx_muxer_t::execute(pal::string_t(), argc, argv, startup_info, nullptr, 0, nullptr);
}
途中の関数では、.NETランタイムのホストモード(host_mode_t
)を決定しています。(後述)
関数呼出しをいくつか挟んで、read_config_and_execute
までたどり着くと、hostpolicy_dir
、corehost_init_t
の取得、初期化をした後、それらを使ってランタイム本体を呼び出します。
int fx_muxer_t::execute(
const pal::string_t host_command, const int argc, const pal::char_t* argv[],
const host_startup_info_t& host_info, pal::char_t result_buffer[], int32_t buffer_size, int32_t* required_buffer_size)
{
// Detect invocation mode
host_mode_t mode = detect_operating_mode(host_info); // host_mode_t::apphost; 後述
int new_argoff;
pal::string_t app_candidate;
opt_map_t opts;
int result = command_line::parse_args_for_mode(mode, host_info, argc, argv, &new_argoff, app_candidate, opts);
// Transform dotnet [exec] [--additionalprobingpath path] [--depsfile file] [dll] [args] -> dotnet [dll] [args]
result = handle_exec_host_command(
host_command, host_info,
app_candidate, opts, argc, argv, new_argoff,
mode, false /*is_sdk_command*/, result_buffer, buffer_size, required_buffer_size);
return result;
}
int fx_muxer_t::handle_exec_host_command(const pal::string_t& host_command, const host_startup_info_t& host_info,
const pal::string_t& app_candidate, const opt_map_t& opts, int argc, const pal::char_t* argv[], int argoff,
host_mode_t mode, const bool is_sdk_command, pal::char_t result_buffer[], int32_t buffer_size, int32_t* required_buffer_size)
{
const pal::char_t** new_argv = argv;
int new_argc = argc;
// Transform dotnet [exec] [--additionalprobingpath path] [--depsfile file] [dll] [args] -> dotnet [dll] [args]
return read_config_and_execute(
host_command, host_info,
app_candidate, opts, new_argc, new_argv,
mode, is_sdk_command, result_buffer, buffer_size, required_buffer_size);
}
int read_config_and_execute(const pal::string_t& host_command, const host_startup_info_t& host_info,
const pal::string_t& app_candidate, const opt_map_t& opts, int new_argc, const pal::char_t** new_argv,
host_mode_t mode, const bool is_sdk_command, pal::char_t out_buffer[], int32_t buffer_size, int32_t* required_buffer_size)
{
pal::string_t hostpolicy_dir;
std::unique_ptr<corehost_init_t> init;
int rc = get_init_info_for_app(
host_command, host_info, app_candidate, opts,
mode, is_sdk_command, hostpolicy_dir, init);
// hostpolicy_dir には host_info.dotnet_root が入ってきています
rc = execute_app(hostpolicy_dir, init.get(), new_argc, new_argv);
return rc;
}
コマンドライン引数について
いろいろ捏ね繰り回しており、関数引数が山ほどあってウンザリします。
やっていること自体はオーソドックスで、引数を辞書構造にパースするだけのものです。
enum class known_options
{
additional_probing_path,
deps_file,
runtime_config,
fx_version,
roll_forward,
additional_deps,
roll_forward_on_no_candidate_fx,
__last // Sentinel value
};
const host_option KnownHostOptions[] =
{
// { pal::char_t* option; pal::char_t* argument; pal::char_t* description; }
{ _X("--additionalprobingpath"), _X("<path>"), _X("Path containing probing policy and assemblies to probe for.") },
{ _X("--depsfile"), _X("<path>"), _X("Path to <application>.deps.json file.") },
{ _X("--runtimeconfig"), _X("<path>"), _X("Path to <application>.runtimeconfig.json file.") },
{ _X("--fx-version"), _X("<version>"), _X("Version of the installed Shared Framework to use to run the application.") },
{ _X("--roll-forward"), _X("<value>"), _X("Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable)") },
{ _X("--additional-deps"), _X("<path>"), _X("Path to additional deps.json file.") },
{ _X("--roll-forward-on-no-candidate-fx"), _X("<n>"), _X("<obsolete>") }
};
// Dictionary<known_options, List<string>> のような型
typedef std::unordered_map<known_options, std::vector<pal::string_t>, known_options_hash> opt_map_t;
int command_line::parse_args_for_mode(host_mode_t mode, const host_startup_info_t &host_info,
const int argc, const pal::char_t *argv[], /*out*/ int *new_argoff,
/*out*/ pal::string_t &app_candidate, /*out*/ opt_map_t &opts, bool args_include_running_executable)
{
// だいたいこんな感じのことをする(LINQ風)
for(int i = 0; i < argc; i += 2)
{
auto option = Enum.GetValues<known_options>()
.First(opt => KnownHostOptions[(int)opt].option == to_lower(argv[i]));
auto values = (*opts)[option];
values.push_back(argv[i + 1]);
(*opts)[option] = values;
}
}
pal::string_t command_line::get_option_value(const opt_map_t &opts, known_options opt, const pal::string_t &default_value)
{
// だいたいこんな感じのことをする(LINQ風)
auto values = opts[opt];
return values.Last();
}
ホストモード(host_mode_t
)
-
host_mode_t::muxer
- "dotnet"コマンドによって起動された場合のモード
-
host_mode_t::apphost
-
corehost
のリネームである、".exe" で起動された場合のモード
-
-
host_mode_t::split_fx
- xunitや、1.x時代に使われていたモード(使うことはまずなさそうです)
-
host_mode_t::libhost
- 例えば、COMActivationやセルフホストのネイティブアプリ(C++/CLIのことかな?)から呼び出されるなど、Exeではない何かから起動された場合のモード
今回の場合は、corehost
をアプリケーション名にリネームするパターンなので、apphost
モードになります。
host_mode_t detect_operating_mode(const host_startup_info_t& host_info)
{
if (coreclr_exists_in_dir(host_info.dotnet_root)) // "<dotnet_root>/coreclr.dll" の有無をチェック
{
// hostfxr_resolver によって解決された dotnet_root 直下に coreclr.dll は普通無い
// .NETのランタイムインストーラを使った場合、"<dotnet_root>/shared/Microsoft.NETCore.App/6.0.0/coreclr.dll" となる
// "<dotnet_root>/<app_name(e.g. ConsoleApp1)>.deps.json" または "./<app_name(e.g. ConsoleApp1)>.runtimeconfig.json"
// が存在し、かつ、app_path が存在すれば apphost
// そうでなければ、split_fx (1.x互換のレガシーモードっぽいので、もう気にしなくてOK?)
return ... ? host_mode_t::apphost : host_mode_t::split_fx;
}
if (pal::file_exists(host_info.app_path))
{
return host_mode_t::apphost; // 今回はコレ!
}
return host_mode_t::muxer; // dotnetCLI起動の場合(e.g. "dotnet ConsoleApp1.dll")
}
hostpolicy_dir
、corehost_init_t
の取得、初期化
hostpolicy_dir
、corehost_init_t
の取得、初期化を行うget_init_info_for_app
は長いですが次のステップで処理を進めます。
hostpolicy.dll
は、hostfxr.dll
と違って、どのバージョン向けのアプリでも1つのもの(最新)が使われるものではなく、バージョンそれぞれに存在するものです。
なので、hostpolicy_dir
を取得するためには、RuntimeConfigなどからターゲットのランタイムのバージョンを決定してあげる必要があります。
RollForwardは、実際に使う.NETランタイムのバージョンの決定ルールを表します。
システムにこのバージョンがインストールされていれば良いですが、.NETは少しだけ異なるバージョンが異なっていてもある程度、互換性があります。
hostfxr
にはこの少しだけ異なるバージョンを柔軟に解決する仕組みがあり、それがRollForwardで、runtimeOptions/rollForward
でルールを指定できます。
「patch/feature/minor/major/latestPatch/latestFeature/latestMinor/latestMajor/disable」
の設定が存在し、"ver...." というバージョン表記に対し、どれぐらいの差異を許容するかというルールです。
このRollForwardの規定はマイナーバージョンです、つまり、メジャーバージョン(5.x/6.x)が一致していないと候補になりません。
RollForwardルールの決定は以下の優先度によって決定されます。(ルールの決定であり実際のバージョン解決はまた別です)
- コマンドライン引数 --roll-forward または --roll-forward-on-no-candidate-fx
- 環境変数 "DOTNET_ROLL_FORWARD"
- runtimeconfig.json の "framework/rollForward" または "framework/rollForwardOnNoCandidateFx"
- runtimeconfig.json の "runtimeOptions/rollForward" または "runtimeOptions/rollForwardOnNoCandidateFx"
- 環境変数 "DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"
RuntimeConfigはデフォルトで以下のような内容になります。
{
"runtimeOptions": {
"tfm": "net6.0",
// "rollForward": "Minor",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0-rc.1.21430.1"
}
}
}
runtimeOptions/framework
が基点となるランタイムバージョンです。
これを基点に、runtimeOptions/rollForward
(未指定ならMinor
)によって、バージョン差が許容内のランタイムを解決します。
RuntimeConfigのパースについての詳細は後述します。
int get_init_info_for_app(const pal::string_t &host_command, const host_startup_info_t &host_info,
const pal::string_t &app_candidate, const opt_map_t &opts, host_mode_t mode, const bool is_sdk_command,
/*out*/ pal::string_t &hostpolicy_dir, /*out*/ std::unique_ptr<corehost_init_t> &init)
{
// --runtimeconfig (runtimeconfig.jsonのパスを引数で指定「できる」(されない))
// --depsfile (deps.jsonのパスを引数で指定「できる」(されない))
// https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet#runtime-options
pal::string_t runtime_config = command_line::get_option_value(opts, known_options::runtime_config, _X(""));
pal::string_t deps_file = command_line::get_option_value(opts, known_options::deps_file, _X(""));
// override_settingは、RuntimeConfigの設定より強い優先度の設定を記録します
// app.m_runtime_config.m_default_settings < app.m_runtime_config < app.m_runtime_config.m_override_settings
runtime_config_t::settings_t override_settings;
// コマンドライン引数 --roll-forward
pal::string_t roll_forward = command_line::get_option_value(opts, known_options::roll_forward, _X(""));
if (roll_forward.length() > 0)
{
override_settings.set_roll_forward(val);
}
// コマンドライン引数 --roll-forward-on-no-candidate-fx
pal::string_t roll_fwd_on_no_candidate_fx = command_line::get_option_value(opts, known_options::roll_forward_on_no_candidate_fx, _X(""));
if (roll_fwd_on_no_candidate_fx.length() > 0)
{
auto val = static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(roll_fwd_on_no_candidate_fx.c_str()));
override_settings.set_roll_forward(roll_fwd_on_no_candidate_fx_to_roll_forward(val));
}
// コマンドライン引数 --runtimeconfig または、
// Managedエントリのアセンブリファイルの横においてある runtimeconfig.json を読み込みます
// (e.g. "./ConsoleApp1.runtimeconfig.json")
// read_config関数によって、RuntimeConfigのパース結果が app.m_runtime_config に設定されます(後述)
fx_definition_vector_t fx_definitions; // リストだけど、基本的には1つだけ(app)です
auto app = new fx_definition_t(); // 候補となるランタイムに対応するオブジェクト?
fx_definitions.push_back(std::unique_ptr<fx_definition_t>(app));
int rc = read_config(*app, app_candidate, runtime_config, override_settings);
// `Roll forward` 決定についてはここまで
runtime_config_t app_config = app->get_runtime_config();
bool is_framework_dependent = app_config.get_is_framework_dependent(); // runtimeconfig.json に framework または frameworks が含まれているか
pal::string_t additional_deps_serialized;
if (is_framework_dependent)
{
// コマンドライン引数 --fx-version があれば、必ずそれを使うようにする
pal::string_t fx_version_specified = command_line::get_option_value(opts, known_options::fx_version, _X(""));
if (fx_version_specified.length() > 0)
{
app_config.set_fx_version(fx_version_specified); // RollForwardをDisableにして、バージョンを指定し、固定する
}
// コマンドライン引数 --additional-deps または、環境変数 DOTNET_ADDITIONAL_DEPS
pal::string_t additional_deps = command_line::get_option_value(opts, known_options::additional_deps, _X(""));
additional_deps_serialized = additional_deps;
if (additional_deps_serialized.empty())
{
pal::getenv(_X("DOTNET_ADDITIONAL_DEPS"), &additional_deps_serialized);
}
// If invoking using FX dotnet.exe, use own directory.
if (mode == host_mode_t::split_fx)
{
auto fx = new fx_definition_t(app_config.get_frameworks()[0].get_fx_name(), host_info.dotnet_root, pal::string_t(), pal::string_t());
fx_definitions.push_back(std::unique_ptr<fx_definition_t>(fx));
}
else
{
// ここまでで掻き集めたRuntimeConfigや引数による設定を加味して、実際に使うランタイムを解決する(後述)
rc = fx_resolver_t::resolve_frameworks_for_app(host_info, override_settings, app_config, fx_definitions);
}
}
std::vector<std::pair<pal::string_t, pal::string_t>> additional_properties;
if (is_sdk_command) { ... } // 今回は通りません
// アプリ側の runtimeconfig.json の runtimeOptions/additionalProbingPaths を解決します
// (通常設定されていないので意味はなさそう?)
const known_options opts_probe_path = known_options::additional_probing_path;
std::vector<pal::string_t> spec_probe_paths = opts.count(opts_probe_path) ? opts.find(opts_probe_path)->second : std::vector<pal::string_t>();
std::vector<pal::string_t> probe_realpaths = get_probe_realpaths(fx_definitions, spec_probe_paths);
// 引数を沢山渡していますが、実際に中でされるのは、hostpolicy_dir に host_info.dotnet_root をコピーするだけです
// hostpolicy_dir = host_info.dotnet_root
if (!hostpolicy_resolver::try_get_dir(mode, host_info.dotnet_root, fx_definitions, app_candidate, deps_file, probe_realpaths, &hostpolicy_dir))
{
return StatusCode::CoreHostLibMissingFailure;
}
// 確定!
init.reset(new corehost_init_t(host_command, host_info, deps_file, additional_deps_serialized, probe_realpaths, mode, fx_definitions, additional_properties));
return StatusCode::Success;
}
int read_config(
fx_definition_t& app, const pal::string_t& app_candidate,
pal::string_t& runtime_config, const runtime_config_t::settings_t& override_settings)
{
// runtime_configまたは、app_candidateの拡張子をjson,dev.jsonとして、config_file, dev_config_fileにセット
pal::string_t config_file, dev_config_file;
get_runtime_config_paths_from_app(runtime_config.empty() ? app_candidate : runtime_config, config_file, dev_config_file);
app.parse_runtime_config(config_file, dev_config_file, override_settings);
return StatusCode::Success;
}
RuntimeConfigのパース
runtime_config_t::parse_opts
がパース処理の中心を担っています。
この関数では、以下のメンバをjsonから読み出し、runtime_config_tのメンバにマッピングします。
this.m_properties = runtimeOptions/configProperties
this.m_probe_paths = runtimeOptions/additionalProbingPaths
this.m_tfm = runtimeOptions/tfm
this.m_frameworks = runtimeOptions/framework, runtimeOptions/frameworks
this.m_included_frameworks = runtimeOptions/includedFrameworks
this.m_default_settings.has_roll_forward = runtimeOptions/rollForward, runtimeOptions/rollForwardOnNoCandidateFx
this.m_default_settings.roll_forward = runtimeOptions/rollForward, runtimeOptions/rollForwardOnNoCandidateFx
this.m_default_settings.has_apply_patches = runtimeOptions/applyPatches
this.m_default_settings.apply_patches = runtimeOptions/applyPatches
この一連のruntime_config_t
のパース処理で注意すべきは、
内部で環境変数DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX
が存在すればそれでRollForwardを設定している点です。
ただし、m_default_settings
への適用であり、優先度としては最低位です。
(m_default_settings
< this
< m_override_settings
)
void fx_definition_t::parse_runtime_config(
const pal::string_t& path, const pal::string_t& dev_path,
const runtime_config_t::settings_t& override_settings)
{
m_runtime_config.parse(path, dev_path, override_settings);
}
void runtime_config_t::parse(const pal::string_t& path, const pal::string_t& dev_path, const settings_t& override_settings)
{
m_path = path;
m_dev_path = dev_path;
m_override_settings = override_settings;
// Step #0: 初期値
m_default_settings.set_apply_patches(true);
roll_forward_option roll_forward = roll_forward_option::Minor;
// Step #1: 環境変数"DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX" が設定されているなら、それでRollForwardを上書きします
pal::string_t env_roll_forward_on_no_candidate_fx;
if (pal::getenv(_X("DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"), &env_roll_forward_on_no_candidate_fx))
{
auto val = static_cast<roll_fwd_on_no_candidate_fx_option>(pal::xtoi(env_roll_forward_on_no_candidate_fx.c_str()));
roll_forward = roll_fwd_on_no_candidate_fx_to_roll_forward(val);
}
m_default_settings.set_roll_forward(roll_forward);
// Parse the file
m_valid = ensure_parsed();
}
bool runtime_config_t::ensure_parsed()
{
json_parser_t json;
json.parse_file(m_path);
const auto& runtimeOpts = json.document().FindMember(_X("runtimeOptions"));
return parse_opts(runtimeOpts->value);
}
ランタイムの解決の引数
ここまで、ランタイムの解決に必要な設定を掻き集めてきました。
ついにそれを使って、実際に使用するランタイムを決定します。
その前に、これらの引数について一旦整理をします。(これは、コード内のコメントの和訳±αです)
host_info
host_path
、dotnet_root
、app_path
をセットにした構造体です。
それぞれ、Exeのパス、ランタイムの検索対象のディレクトリ、Managedエントリのアセンブリのパスを示しています。
dotnet_root
の下には、バージョンごとのディレクトリが切られていて、その中にそれぞれバージョンの異なるランタイムがインストールされていることが期待されます。
struct host_startup_info_t
{
pal::string_t host_path; // The path to the current hosting binary.
pal::string_t dotnet_root; // The path to the framework.
pal::string_t app_path; // For apphost, the path to the app dll; for muxer, not applicable as this information is not yet parsed.
};
override_settings
最優先で考慮される、ランタイムの解決に用いる設定です。(コマンドライン引数による設定)
fx_reference_t
で渡された構造体のバージョンを指定する部分は無視されます。
struct runtime_config_t::settings_t
{
bool has_apply_patches;
bool apply_patches;
bool has_roll_forward;
roll_forward_option roll_forward;
};
config
この中にもm_override_settings
が含まれていますが、生で渡されたoverride_settings
と同じインスタンスであり、直接使われるのは後者の方となります。
".runtimeconfig.json"による設定をパースした構造体です。
class runtime_config_t
{
std::unordered_map<pal::string_t, pal::string_t> m_properties;
fx_reference_vector_t m_frameworks;
fx_reference_vector_t m_included_frameworks;
settings_t m_default_settings; // the default settings (Steps #0 and #1)
settings_t m_override_settings; // the settings that can't be changed (Step #5)
std::vector<std::string> m_prop_keys;
std::vector<std::string> m_prop_values;
std::list<pal::string_t> m_probe_paths;
pal::string_t m_tfm;
// This is used to detect cases where rollForward is used together with the obsoleted
// rollForwardOnNoCandidateFx/applyPatches.
// Flags
enum specified_setting
{
none = 0x0,
specified_roll_forward = 0x1,
specified_roll_forward_on_no_candidate_fx_or_apply_patched = 0x2
} m_specified_settings;
pal::string_t m_dev_path;
pal::string_t m_path;
bool m_is_framework_dependent;
bool m_valid;
}
class fx_reference_t
{
bool apply_patches;
version_compatibility_range_t version_compatibility_range;
bool roll_to_highest_version;
// This indicates that when resolving the framework reference the search should prefer release version
// and only resolve to pre-release if there's no matching release version available.
bool prefer_release;
pal::string_t fx_name;
pal::string_t fx_version;
fx_ver_t fx_version_number;
};
effective_parent_fx_ref
親ランタイムの情報です。
これからランタイムを解決するわけですが、その解決をしようとしているランタイムの情報を渡せるとのことらしいです。
今回はnullptr
が渡されているので考慮する必要はありません。
fx_definitions
見つかったランタイムが格納されていく、呼出し側が結果を受け取るためのリストです。
順序は、アプリ(末端)→ ルートフレームワーク(Microsoft.NETCore.App
)の並びになります。
1つ目の要素には、RuntimeConfig を読み込んだ app
(fx_definition_t
)がセットされています。
ただし、RuntimeConfigとDeps以外のメンバはセットされていないので、無効扱いです。(しかもそのRuntimeConfigは別引数で渡されている)
class fx_definition_t
{
pal::string_t m_name;
pal::string_t m_dir;
pal::string_t m_requested_version;
pal::string_t m_found_version;
runtime_config_t m_runtime_config;
pal::string_t m_deps_file;
deps_json_t m_deps;
};
typedef std::vector<std::unique_ptr<fx_definition_t>> fx_definition_vector_t;
ランタイムの解決処理
実際に使用するランタイムを決定します。
引数を捏ね繰り回して、最終的に、fx_definitions
に解決されたランタイムが追加されます
StatusCode fx_resolver_t::resolve_frameworks_for_app(
const host_startup_info_t & host_info, const runtime_config_t::settings_t& override_settings,
const runtime_config_t & app_config, fx_definition_vector_t & fx_definitions)
{
fx_resolver_t resolver;
// Read the shared frameworks; retry is necessary when a framework is already resolved, but then a newer compatible version is processed.
fx_definitions.resize(1); // Erase any existing frameworks for re-try
rc = resolver.read_framework(host_info, override_settings, app_config, /*effective_parent_fx_ref*/ nullptr, fx_definitions);
// 本来は、resolver.read_framework が StatusCode::FrameworkCompatRetryを返してくることがあり、その際のリトライ処理があります
return rc;
}
StatusCode fx_resolver_t::read_framework(const host_startup_info_t & host_info,
const runtime_config_t::settings_t& override_settings, const runtime_config_t & config,
const fx_reference_t * effective_parent_fx_ref, fx_definition_vector_t & fx_definitions)
{
// 辞書構造の fx_resolver_t::m_effective_fx_references, fx_resolver_t::m_oldest_fx_references に追加します
// This reconciles duplicate references to minimize the number of resolve retries.
update_newest_references(config);
StatusCode rc = StatusCode::Success;
// Loop through each reference and resolve the framework
for (const fx_reference_t& original_fx_ref : config.get_frameworks())
{
fx_reference_t fx_ref = original_fx_ref;
if (effective_parent_fx_ref != nullptr && effective_parent_fx_ref->get_roll_to_highest_version())
{
// 親ランタイムの roll_to_highest_version で上書きします
fx_ref.set_roll_to_highest_version(true);
}
const pal::string_t& fx_name = fx_ref.get_fx_name();
const fx_reference_t& current_effective_fx_ref = m_effective_fx_references[fx_name];
fx_reference_t new_effective_fx_ref;
// fx_definitionsの1つ目はセットされていますが、無効なのでこの検索は見つかりません
auto existing_framework = std::find_if(
fx_definitions.begin(),
fx_definitions.end(),
[&](const std::unique_ptr<fx_definition_t> & fx) { return fx_name == fx->get_name(); });
if (existing_framework == fx_definitions.end())
{
// 以下のようなことをしています
// m_effective_fx_references[fx_name] = fx_ref.fx_version_number > current_effective_fx_ref.fx_version_number ? fx_ref : current_effective_fx_ref
// 今回は current_effective_fx_ref は fx_ref と同じなので、fx_ref、つまり RuntimeConfig の runtimeOptions/framework になります
// Reconcile the framework reference with the most up to date so far we have for the framework.
// This does not read any physical framework folders yet.
// Since we didn't find the framework in the resolved list yet, it's OK to update the effective reference
// as we haven't processed it yet.
rc = reconcile_fx_references(fx_ref, current_effective_fx_ref, new_effective_fx_ref);
m_effective_fx_references[fx_name] = new_effective_fx_ref;
// この関数が、実際にランタイムのインストールディレクトリを舐めて、
// インストールされているバージョン一覧を取得し、RollForwardを考慮してバージョンとそのランタイムのパスを決定します
// Resolve the effective framework reference against the the existing physical framework folders
fx_definition_t* fx = resolve_framework_reference(new_effective_fx_ref, m_oldest_fx_references[fx_name].get_fx_version(), host_info.dotnet_root);
// Do NOT update the effective reference to have the same version as the resolved framework.
// This could prevent correct resolution in some cases.
// For example if the resolution starts with reference "2.1.0 LatestMajor" the resolution could
// return "3.0.0". If later on we find another reference "2.1.0 Minor", while the two references are compatible
// we would not be able to resolve it, since we would compare "2.1.0 Minor" with "3.0.0 LatestMajor" which are
// not compatible.
// So instead leave the effective reference as is. If the above situation occurs, the reference reconciliation
// will change the effective reference from "2.1.0 LatestMajor" to "2.1.0 Minor" and restart the framework resolution process.
// So during the second run we will resolve for example "2.2.0" which will be compatible with both framework references.
fx_definitions.push_back(std::unique_ptr<fx_definition_t>(fx));
// 見つかったランタイムの runtimeconfig.json を読み込みます
// (これが存在し、この中に runtimeOptions/framework が設定されていると、再帰的に検索されるってコト?)
// Recursively process the base frameworks
pal::string_t config_file;
pal::string_t dev_config_file;
get_runtime_config_paths(fx->get_dir(), fx_name, &config_file, &dev_config_file);
fx->parse_runtime_config(config_file, dev_config_file, override_settings);
// 再帰呼び出し; 今回は、new_config.frameworks が空なので何もせずに制御が帰ってきます
runtime_config_t new_config = fx->get_runtime_config();
rc = read_framework(host_info, override_settings, new_config, &new_effective_fx_ref, fx_definitions);
}
else
{
// fx_definitions に解決済みのランタイム情報が設定されている場合; 今回は通りません
}
}
return rc;
}
fx_definition_t* resolve_framework_reference(const fx_reference_t & fx_ref,
const pal::string_t & oldest_requested_version, const pal::string_t & dotnet_dir)
{
// dotnet_dir(corehost で決定した dotnet_root)と、
// システムにインストールされている.NETランタイムの場所を取得してリストにします
std::vector<pal::string_t> hive_dir;
get_framework_and_sdk_locations(dotnet_dir, &hive_dir);
pal::string_t selected_fx_dir;
pal::string_t selected_fx_version;
fx_ver_t selected_ver;
for (pal::string_t dir : hive_dir)
{
auto fx_dir = /* "<dir>/shared/Microsoft.NETCore.App" */;
// Roll forward is disabled when:
// roll_forward is set to Disable
// roll_forward is set to LatestPatch AND
// apply_patches is false AND
// release framework reference (this is for backward compat with pre-release rolling over pre-release portion of version ignoring apply_patches)
// use exact version is set (this is when --fx-version was used on the command line)
if ((fx_ref.get_version_compatibility_range() == version_compatibility_range_t::exact) ||
((fx_ref.get_version_compatibility_range() == version_compatibility_range_t::patch) && (!fx_ref.get_apply_patches() && !fx_ref.get_fx_version_number().is_prerelease())))
{
// RollForwardが無効の場合、"<dir>/shared/Microsoft.NETCore.App/<version>" ディレクトリをランタイムの場所として解決します
// ディレクトリが存在しない場合は当然エラーになるけど
append_path(&fx_dir, fx_ref.get_fx_version().c_str());
if (pal::directory_exists(fx_dir))
{
selected_fx_dir = fx_dir;
selected_fx_version = fx_ref.get_fx_version();
break;
}
}
else
{
// fx_dir ディレクトリ下のディレクトリを舐めて、インストールされているランタイムのバージョンのリストを構築します
std::vector<fx_ver_t> version_list = ...;
// version_list から RollForwardの条件を満たすバージョンを選択します
fx_ver_t resolved_ver = resolve_framework_reference_from_version_list(version_list, fx_ref);
pal::string_t resolved_ver_str = resolved_ver.as_str();
append_path(&fx_dir, resolved_ver_str.c_str());
selected_ver = resolved_ver;
selected_fx_dir = fx_dir;
selected_fx_version = resolved_ver_str;
// 本来は hive_dir が複数あることを考慮して、ひとつ前のLoopの際の selected_ver との比較処理があります
}
}
return new fx_definition_t(fx_ref.get_fx_name(), selected_fx_dir, oldest_requested_version, selected_fx_version);
}
.NETランタイム本体の起動をする hostpolicy.dll
の起動
ここで、コンテキストは hostfxr.dll
からhostpolicy.dll
へ移る呼出しを行います。
呼出しは2段階に分かれていて、それぞれはグローバル変数で協調されるということです。
そのため、hostfxr.dll
側でアトミックを保証する処理が含まれ煩雑になっています。
-
corehost_init_t
の転送(hostpolicy.dll!corehost_load
) - 実行(
hostpolicy.dll!corehost_main
)
// Tracks the active host context. This is the context that was used to load and initialize hostpolicy and coreclr.
// It will only be set once both hostpolicy and coreclr are loaded and initialized. Once set, it should not be changed.
// This will remain set even if the context is closed through hostfxr_close. Since the context represents the active
// CoreCLR runtime and the active runtime cannot be unloaded, the active context is never unset.
std::unique_ptr<host_context_t> g_active_host_context;
// impl_dll_dir は dotnet_root が来ます(corehost が解決したランタイムのインストール場所; バージョンディレクトリの上、sharedの上)
static int execute_app(const pal::string_t& impl_dll_dir, corehost_init_t* init, const int argc, const pal::char_t* argv[])
{
{
std::unique_lock<std::mutex> lock{ g_context_lock };
g_context_initializing_cv.wait(lock, [] { return !g_context_initializing.load(); });
if (g_active_host_context != nullptr)
{
trace::error(_X("Hosting components are already initialized. Re-initialization to execute an app is not allowed."));
return StatusCode::HostInvalidState;
}
g_context_initializing.store(true);
}
pal::dll_t hostpolicy_dll;
hostpolicy_contract_t hostpolicy_contract{};
corehost_main_fn host_main = nullptr;
int code = load_hostpolicy(impl_dll_dir, &hostpolicy_dll, hostpolicy_contract);
// Obtain entrypoint symbol
host_main = hostpolicy_contract.corehost_main;
// Leak hostpolicy - just as we do not unload coreclr, we do not unload hostpolicy
{
// Track an empty 'active' context so that host context-based APIs can work properly when
// the runtime is loaded through non-host context-based APIs. Once set, the context is never
// unset. This means that if any error occurs after this point (e.g. with loading the runtime),
// the process will be in a corrupted state and loading the runtime again will not be allowed.
std::lock_guard<std::mutex> lock{ g_context_lock };
assert(g_active_host_context == nullptr);
g_active_host_context.reset(new host_context_t(host_context_type::empty, hostpolicy_contract, {}));
g_active_host_context->initialize_frameworks(*init); // init から g_active_host_context へのコピー
g_context_initializing.store(false);
}
g_context_initializing_cv.notify_all();
{
// hostpolicy.dll!corehost_load で、ランタイムの起動用の情報群(corehost_init_t のインターフェイス)を転送
// https://github.com/dotnet/runtime/blob/v6.0.0/src/native/corehost/hostpolicy/hostpolicy.cpp#L304
const host_interface_t& intf = init->get_host_init_data();
hostpolicy_contract.load(&intf);
// hostpolicy.dll!corehost_main を実行!
// https://github.com/dotnet/runtime/blob/v6.0.0/src/native/corehost/hostpolicy/hostpolicy.cpp#L414
code = host_main(argc, argv);
(void)hostpolicy_contract.unload();
}
return code;
}
int load_hostpolicy(const pal::string_t& lib_dir, pal::dll_t* h_host, hostpolicy_contract_t& hostpolicy_contract)
{
int rc = hostpolicy_resolver::load(lib_dir, h_host, hostpolicy_contract); // 後述
return StatusCode::Success;
}
hostpolicy.dll
のロード
hostpolicy.dll
のエクスポート関数を hostpolicy_contract_t
にマッピングします
// impl_dll_dir は dotnet_root が来ます(corehost が解決したランタイムのインストール場所; バージョンディレクトリの上、sharedの上)
int hostpolicy_resolver::load(const pal::string_t& lib_dir, pal::dll_t* dll, hostpolicy_contract_t &hostpolicy_contract)
{
std::lock_guard<std::mutex> lock{ g_hostpolicy_lock };
if (g_hostpolicy == nullptr)
{
pal::string_t host_path = /* "<lib_dir>"/hostpolicy.dll */ ;
pal::load_library(&host_path, &g_hostpolicy);
// Obtain entrypoint symbols
g_hostpolicy_contract.corehost_main = reinterpret_cast<corehost_main_fn>(pal::get_symbol(g_hostpolicy, "corehost_main"));
g_hostpolicy_contract.load = reinterpret_cast<corehost_load_fn>(pal::get_symbol(g_hostpolicy, "corehost_load"));
g_hostpolicy_contract.unload = reinterpret_cast<corehost_unload_fn>(pal::get_symbol(g_hostpolicy, "corehost_unload"));
g_hostpolicy_contract.corehost_main_with_output_buffer = reinterpret_cast<corehost_main_with_output_buffer_fn>(pal::get_symbol(g_hostpolicy, "corehost_main_with_output_buffer"));
// It's possible to not have corehost_main_with_output_buffer.
// This was introduced in 2.1, so 2.0 hostpolicy would not have the exports.
// Callers are responsible for checking that the function pointer is not null before using it.
g_hostpolicy_contract.set_error_writer = reinterpret_cast<corehost_set_error_writer_fn>(pal::get_symbol(g_hostpolicy, "corehost_set_error_writer"));
g_hostpolicy_contract.initialize = reinterpret_cast<corehost_initialize_fn>(pal::get_symbol(g_hostpolicy, "corehost_initialize"));
// It's possible to not have corehost_set_error_writer and corehost_initialize. These were
// introduced in 3.0, so 2.0 hostpolicy would not have the exports. In this case, we will
// not propagate the error writer and errors will still be reported to stderr. Callers are
// responsible for checking that the function pointers are not null before using them.
g_hostpolicy_dir = lib_dir;
}
else
{
// ロード済みの場合(今回はとおりません)
}
// Return global values
*dll = g_hostpolicy;
hostpolicy_contract = g_hostpolicy_contract;
return StatusCode::Success;
}
hostpolicy.dll
hostfxr.dll
から、2段階でパラメータの転送、(hostpolicy.dll!corehost_load
)実行(hostpolicy.dll!corehost_main
)が呼び出されるところから始まります。
hostpolicy.dll
は、<DOTNET_ROOT>\shared\Microsoft.NETCore.App\6.0.0\
下にあるモジュールです。つまり、hostfxr
で解決されたランタイムバージョンごとにhostpolicy.dll
が存在します。(逆にhostfxr.dll
はランタイムのバージョンとは関係なく存在しています)
hostpolicy
で行う処理は主に、hostfxr
から受け取ったパラメータをcoreclr
が求める形に成形し、coreclr
を初期化(coreclr_initialize
)してManagedエントリを実行する関数(execute_assembly
)を実行させます。
パラメータ(host_interface_t
)の受け取り
まずはパラメータについてですが、これはhostfxr
で解釈した引数やRuntimeConfig、Deps、決定したランタイムのバージョン、パスなどに、バリデーションデータを追加した構造になっています。
struct host_interface_t
{
size_t version_lo; // Just assign sizeof() to this field.
size_t version_hi; // Breaking changes to the layout -- increment HOST_INTERFACE_LAYOUT_VERSION
strarr_t config_keys;
strarr_t config_values;
const pal::char_t* fx_dir;
const pal::char_t* fx_name;
const pal::char_t* deps_file;
size_t is_framework_dependent;
strarr_t probe_paths;
size_t patch_roll_forward;
size_t prerelease_roll_forward;
size_t host_mode;
const pal::char_t* tfm;
const pal::char_t* additional_deps_serialized;
const pal::char_t* fx_ver;
strarr_t fx_names;
strarr_t fx_dirs;
strarr_t fx_requested_versions;
strarr_t fx_found_versions;
const pal::char_t* host_command;
const pal::char_t* host_info_host_path;
const pal::char_t* host_info_dotnet_root;
const pal::char_t* host_info_app_path;
size_t single_file_bundle_header_offset;
// !! WARNING / WARNING / WARNING / WARNING / WARNING / WARNING / WARNING / WARNING / WARNING
// !! 1. Only append to this structure to maintain compat.
// !! 2. Any nested structs should not use compiler specific padding (pack with _HOST_INTERFACE_PACK)
// !! 3. Do not take address of the fields of this struct or be prepared to deal with unaligned accesses.
// !! 4. Must be POD types; only use non-const size_t and pointer types; no access modifiers.
// !! 5. Do not reorder fields or change any existing field types.
// !! 6. Add static asserts for fields you add.
};
実際に呼び出される関数はこのような定義担っています。host_interface_t
をhostpolicy_init_t
にコピー(+解釈)して、Global変数にセットします。
Global変数は、hostfxr
からの2段目の呼出し(hostpolicy.dll!corehost_main
)で使われます。
(何故わざわざGlobal変数を使ってまで、2段階に分けてるのかは謎)
SHARED_API int HOSTPOLICY_CALLTYPE corehost_load(host_interface_t* init)
{
std::lock_guard<std::mutex> lock{ g_init_lock };
g_init = hostpolicy_init_t{};
hostpolicy_init_t::init(init, &g_init);
g_init_done = true;
return StatusCode::Success;
}
host_interface_t
からhostpolicy_init_t
へのコピー(+解釈)は比較的素直な処理担っています。
おおよそ、host_interface_t* input
のメンバをhostpolicy_init_t* init
にディープコピーしている感じです。
make_palstr_arr
は、Interface表現のリストをstd::vector
にデシリアライズ(?)しているだけなので、分かりやすいように代入式に書き換えています。
また、旧バージョンのhostfxr
との分岐処理も省いています。
bool hostpolicy_init_t::init(host_interface_t* input, hostpolicy_init_t* init)
{
// host_interface_t::version_hi, host_interface_t::version_lo のチェックを省略
init->cfg_keys = input->config_keys;
init->cfg_values = input->config_values;
init->deps_file = input->deps_file;
init->is_framework_dependent = input->is_framework_dependent;
init->probe_paths = input->probe_paths;
init->patch_roll_forward = input->patch_roll_forward;
init->prerelease_roll_forward = input->prerelease_roll_forward;
init->host_mode = (host_mode_t)input->host_mode;
init->tfm = input->tfm;
init->additional_deps_serialized = input->additional_deps_serialized;
for (size_t i = 0; i < input->fx_names.len; ++i)
{
auto fx = new fx_definition_t(input->fx_names[i], input->fx_dirs[i], input->fx_requested_versions[i], input->fx_found_versions[i]);
init->fx_definitions.push_back(std::unique_ptr<fx_definition_t>(fx));
}
init->host_command = input->host_command;
init->host_info.host_path = input->host_info_host_path;
init->host_info.dotnet_root = input->host_info_dotnet_root;
init->host_info.app_path = input->host_info_app_path;
return true;
}
coreclr
の立ち上げの実行(hostpolicy.dll!corehost_main
)
SHARED_API int HOSTPOLICY_CALLTYPE corehost_main(const int argc, const pal::char_t* argv[])
{
// これは、SingleFileMode用の処理やTracerの初期化など; 今回は重要な処理を含みません
int rc = corehost_main_init(g_init, argc, argv, _X("corehost_main"));
arguments_t args;
rc = create_hostpolicy_context(g_init, argc, argv, true /* breadcrumbs_enabled */, &args);
rc = create_coreclr();
return run_app(args.app_argc, args.app_argv);
}
int create_hostpolicy_context(hostpolicy_init_t &hostpolicy_init, const int argc, const pal::char_t *argv[],
bool breadcrumbs_enabled, /*out*/ arguments_t *out_args = nullptr)
{
// g_context_initializingのMutexロック省略
// hostpolicy_init, argc, argvを、argsの各メンバにコピー
arguments_t args;
parse_arguments(hostpolicy_init, argc, argv, args);
if (out_args != nullptr) *out_args = args;
std::unique_ptr<hostpolicy_context_t> context_local(new hostpolicy_context_t());
int rc = context_local->initialize(hostpolicy_init, args, breadcrumbs_enabled);
g_context.reset(context_local.release());
// g_context_initializingのMutexリリース省略
return StatusCode::Success;
}
struct arguments_t
{
host_mode_t host_mode;
pal::string_t host_path;
pal::string_t app_root;
pal::string_t deps_path;
pal::string_t core_servicing;
std::vector<pal::string_t> probe_paths;
pal::string_t managed_application;
std::vector<pal::string_t> global_shared_stores;
pal::string_t dotnet_shared_store;
std::vector<pal::string_t> env_shared_store;
pal::string_t additional_deps_serialized;
}
hostpolicy_context_t
初期化
hostpolicy_context_t::initialize
は主に2つの処理を行います。
まずはdeps_resolver_t::resolve_probe_paths
でprobe_paths_t
を解決します。
その後、これをcoreclr_properties
に積み替えを行います。
breadcrumbs
というのがありますが、これはTracer用途っぽいので、深く考えなくてもよさそうです。
struct hostpolicy_context_t
{
pal::string_t application;
pal::string_t clr_dir;
pal::string_t clr_path;
host_mode_t host_mode;
pal::string_t host_path;
bool breadcrumbs_enabled;
mutable std::unordered_set<pal::string_t> breadcrumbs;
coreclr_property_bag_t coreclr_properties;
std::unique_ptr<coreclr_t> coreclr;
};
int hostpolicy_context_t::initialize(hostpolicy_init_t &hostpolicy_init, const arguments_t &args, bool enable_breadcrumbs)
{
application = args.managed_application; // Managedエントリのアセンブリ(e.g. L"C:\ConsoleApp1\ConsoleApp1.dll")
host_mode = hostpolicy_init.host_mode;
host_path = args.host_path; // 起動したExe(e.g. L"C:\ConsoleApp1\ConsoleApp1.exe")
breadcrumbs_enabled = enable_breadcrumbs; // true
deps_resolver_t resolver
{
args,
hostpolicy_init.fx_definitions,
/* root_framework_rid_fallback_graph */ nullptr, // This means that the fx_definitions contains the root framework
hostpolicy_init.is_framework_dependent
};
probe_paths_t probe_paths;
// Setup breadcrumbs.
pal::string_t policy_name = _STRINGIFY(HOST_POLICY_PKG_NAME); // L"runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy"
pal::string_t policy_version = _STRINGIFY(HOST_POLICY_PKG_VER); // L"6.0.0"
// Always insert the hostpolicy that the code is running on.
breadcrumbs.insert(policy_name);
breadcrumbs.insert(policy_name + _X(",") + policy_version);
resolver.resolve_probe_paths(&probe_paths, &breadcrumbs /* enable_breadcrumbs==falseの時はnullptr */);
// self-contained の場合、CoreLib.dll はBundleに入っていることが期待されるので、この辺はされなくなります
clr_path = probe_paths.coreclr;
clr_dir = get_directory(clr_path); // Get path in which CoreCLR is present.
probe_paths.tpa.append(clr_dir + "System.Private.CoreLib.dll");
const fx_definition_vector_t &fx_definitions = resolver.get_fx_definitions();
pal::string_t fx_deps_str = get_root_framework(fx_definitions).get_deps_file(); // Microsoft.NETCore.App.deps.json"
// resolver.m_fx_definitions のそれぞれの deps.json を取得します
// e.g. "C:\...\ConsoleApp1.deps.json;C:\...\Microsoft.NETCore.App.deps.json"
pal::string_t app_context_deps_str = ...;
// Build properties for CoreCLR instantiation
pal::string_t app_base;
resolver.get_app_dir(&app_base);
coreclr_properties.add(common_property::TrustedPlatformAssemblies, probe_paths.tpa.c_str());
coreclr_properties.add(common_property::NativeDllSearchDirectories, probe_paths.native.c_str());
coreclr_properties.add(common_property::PlatformResourceRoots, probe_paths.resources.c_str());
coreclr_properties.add(common_property::AppContextBaseDirectory, app_base.c_str());
coreclr_properties.add(common_property::AppContextDepsFiles, app_context_deps_str.c_str());
coreclr_properties.add(common_property::FxDepsFile, fx_deps_str.c_str());
coreclr_properties.add(common_property::ProbingDirectories, resolver.get_lookup_probe_directories().c_str());
coreclr_properties.add(common_property::RuntimeIdentifier, get_current_runtime_id(true /*use_fallback*/).c_str());
// 今回は、以下のPropertyは処理されません
// // hostpolicy_init.cfg_keys に Microsoft.NETCore.DotNetHostPolicy.SetAppPaths を反映します
// if (...)
// {
// coreclr_properties.add(common_property::AppPaths, app_base.c_str());
// coreclr_properties.add(common_property::AppNIPaths, app_base.c_str());
// }
// pal::string_t startup_hooks;
// if (pal::getenv(_X("DOTNET_STARTUP_HOOKS"), &startup_hooks))
// {
// coreclr_properties.add(common_property::StartUpHooks, startup_hooks.c_str());
// }
// if (bundle::info_t::is_single_file_bundle())
// {
// // Single-File の場合、common_property::BundleProbeに&bundle_probe(ポインタアドレス)をセット
// coreclr_properties.add(common_property::BundleProbe, $"0x{&bundle_probe}");
// }
return StatusCode::Success;
}
deps_resolver_t::resolve_probe_paths
deps.jsonに記述されたManagedアセンブリのことを、TPA(Trusted Platform Assemblies)と呼ぶそうです。
bool deps_resolver_t::resolve_probe_paths(probe_paths_t* probe_paths, std::unordered_set<pal::string_t>* breadcrumb, bool ignore_missing_assemblies)
{
// deps.json による依存しているManagedアセンブリのDllを解決します
resolve_tpa_list(&probe_paths->tpa, breadcrumb, ignore_missing_assemblies);
// deps.json による依存しているNativeモジュールのDllを解決します(nativeに入るのはそのディレクトリ)
resolve_probe_dirs(deps_entry_t::asset_types::native, &probe_paths->native, breadcrumb);
// deps.json による依存しているリソースを解決します(resourcesに入るのはそのディレクトリのディレクトリ)
resolve_probe_dirs(deps_entry_t::asset_types::resources, &probe_paths->resources, breadcrumb);
// If we found coreclr and the jit during native path probe, set the paths now.
probe_paths->coreclr = m_coreclr_path;
return true;
}
create_coreclr
coreclr.dll!coreclr_initialize
を呼び出します。
引数にはありませんが、Global変数を経由して先ほど初期化したhostpolicy_context_t
を利用します。
int HOSTPOLICY_CALLTYPE create_coreclr()
{
coreclr_bind(libcoreclr_path);
std::vector<char> host_path;
pal::pal_clrstring(g_context->host_path, &host_path);
const char *app_domain_friendly_name = g_context->host_mode == host_mode_t::libhost ? "clr_libhost" : "clrhost";
// Create a CoreCLR instance
auto hr = coreclr_t::create(g_context->clr_dir, host_path.data(), app_domain_friendly_name,
g_context->coreclr_properties, g_context->coreclr);
}
bool coreclr_bind(const pal::string_t& libcoreclr_path)
{
coreclr_resolver_t::resolve_coreclr(libcoreclr_path, coreclr_contract);
return true;
}
bool coreclr_resolver_t::resolve_coreclr(const pal::string_t& libcoreclr_path, coreclr_resolver_contract_t& coreclr_resolver_contract)
{
coreclr_resolver_contract.coreclr = nullptr;
coreclr_resolver_contract.coreclr_initialize = reinterpret_cast<coreclr_initialize_fn>(coreclr_initialize);
coreclr_resolver_contract.coreclr_shutdown = reinterpret_cast<coreclr_shutdown_fn>(coreclr_shutdown_2);
coreclr_resolver_contract.coreclr_execute_assembly = reinterpret_cast<coreclr_execute_assembly_fn>(coreclr_execute_assembly);
coreclr_resolver_contract.coreclr_create_delegate = reinterpret_cast<coreclr_create_delegate_fn>(coreclr_create_delegate);
return true;
}
pal::hresult_t coreclr_t::create(
const pal::string_t& libcoreclr_path,
const char* exe_path,
const char* app_domain_friendly_name,
const coreclr_property_bag_t &properties,
std::unique_ptr<coreclr_t> &inst)
{
host_handle_t host_handle;
domain_id_t domain_id;
int propertyCount = properties.count();
std::vector<std::vector<char>> keys_strs(propertyCount);
std::vector<const char*> keys(propertyCount);
std::vector<std::vector<char>> values_strs(propertyCount);
std::vector<const char*> values(propertyCount);
int index = 0;
std::function<void (const pal::string_t &,const pal::string_t &)> callback = [&] (const pal::string_t& key, const pal::string_t& value)
{
pal::pal_clrstring(key, &keys_strs[index]);
keys[index] = keys_strs[index].data();
pal::pal_clrstring(value, &values_strs[index]);
values[index] = values_strs[index].data();
++index;
};
properties.enumerate(callback);
pal::hresult_t hr;
hr = coreclr_contract.coreclr_initialize(
exe_path, app_domain_friendly_name, propertyCount,
keys.data(), values.data(), &host_handle, &domain_id);
inst.reset(new coreclr_t(host_handle, domain_id));
return StatusCode::Success;
}
run_app
coreclr.dll!execute_assembly
を呼び出します。
引数にはありませんが、Global変数を経由して先ほど初期化したhostpolicy_context_t
を利用します。
int HOSTPOLICY_CALLTYPE run_app(const int argc, const pal::char_t *argv[])
{
const std::shared_ptr<hostpolicy_context_t> context = g_context; // 本来はMutex操作を伴う
return run_app_for_context(*context, argc, argv);
}
int run_app_for_context(const hostpolicy_context_t &context, int argc, const pal::char_t **argv)
{
// Initialize clr strings for arguments
std::vector<std::vector<char>> argv_strs = argv; // 本来は->vec変換
std::vector<const char*> argv_local = argv; // 本来は->vec変換
std::vector<char> managed_app = context.application; // 本来はstr->vec<char>変換
// Execute the application
unsigned int exit_code;
auto hr = context.coreclr->execute_assembly(
(int32_t)argv_local.size(), argv_local.data(),
managed_app.data(), &exit_code);
// Shut down the CoreCLR
hr = context.coreclr->shutdown(reinterpret_cast<int*>(&exit_code));
return exit_code;
}
pal::hresult_t coreclr_t::execute_assembly(int argc, const char** argv,
const char* managed_assembly_path, unsigned int* exit_code)
{
// Managedプログラムが稼働している間、execute_assembly がブロックし続けています
// つまり、間接的にProgram.Main をよびだします
return coreclr_contract.coreclr_execute_assembly(_host_handle, _domain_id, argc, argv, managed_assembly_path, exit_code);
}
coreclr.dll
スタックトレースにはみえていませんでしたが、execute_assembly
とMain
の間にcoreclr
の関数が存在しています。
(VisualStudioがManagedの一部として省略しちゃうのかな?)
言語 | モジュール | 関数 |
---|---|---|
C# | ConsoleApp1.dll | ConsoleApp1.Program.Main |
C++ | coreclr.dll | この間にも更にCLIのVMの処理がある |
C++ | coreclr.dll | RunMainInternal |
C++ | coreclr.dll | RunMain |
C++ | coreclr.dll | Assembly::ExecuteMainMethod |
C++ | coreclr.dll | CorHost2::ExecuteAssembly |
C++ | coreclr.dll | coreclr_execute_assembly |
C++ | hostpolicy.dll | coreclr_t::execute_assembly |
coreclr
は、ランタイムの中核を成す、本体ともいうべきモジュールです。CLIの型メタデータを管理したり、JIT、VM、GCなどの管理も行います。
coreclr_initialize
IID_ICLRRuntimeHost4
を取得し、各初期化メソッドを呼び出していきます。
既にかなり長くなってしまっているので、この中については詳細は割愛しますが、COM風になっているだけで別にCOMを介しているわけではありません。(名残と互換性?)
内部では、ppUnk = (ICLRRuntimeHost4)new CorHost2();
としているだけです。
extern "C" DLLEXPORT int coreclr_initialize(
const char* exePath, const char* appDomainFriendlyName, int propertyCount,
const char** propertyKeys, const char** propertyValues, void** hostHandle, unsigned int* domainId)
{
HRESULT hr;
LPCWSTR* propertyKeysW;
LPCWSTR* propertyValuesW;
BundleProbeFn* bundleProbe = nullptr;
bool hostPolicyEmbedded = false;
PInvokeOverrideFn* pinvokeOverride = nullptr;
ConvertConfigPropertiesToUnicode(propertyKeys, propertyValues, propertyCount, &propertyKeysW, &propertyValuesW, &bundleProbe, &pinvokeOverride, &hostPolicyEmbedded);
g_hostpolicy_embedded = hostPolicyEmbedded;
if (pinvokeOverride != nullptr)
{
PInvokeOverride::SetPInvokeOverride(pinvokeOverride, PInvokeOverride::Source::RuntimeConfiguration);
}
ReleaseHolder<ICLRRuntimeHost4> host;
hr = CorHost2::CreateObject(IID_ICLRRuntimeHost4, (void**)&host); // 中で、QueryInterface
IfFailRet(hr);
ConstWStringHolder appDomainFriendlyNameW = StringToUnicode(appDomainFriendlyName);
if (bundleProbe != nullptr)
{
static Bundle bundle(exePath, bundleProbe);
Bundle::AppBundle = &bundle;
}
// This will take ownership of propertyKeysWTemp and propertyValuesWTemp
Configuration::InitializeConfigurationKnobs(propertyCount, propertyKeysW, propertyValuesW);
STARTUP_FLAGS startupFlags;
InitializeStartupFlags(&startupFlags);
hr = host->SetStartupFlags(startupFlags);
IfFailRet(hr);
hr = host->Start();
IfFailRet(hr);
hr = host->CreateAppDomainWithManager(
appDomainFriendlyNameW,
// Flags:
// APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS
// - By default CoreCLR only allows platform neutral assembly to be run. To allow
// assemblies marked as platform specific, include this flag
//
// APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP
// - Allows sandboxed applications to make P/Invoke calls and use COM interop
//
// APPDOMAIN_SECURITY_SANDBOXED
// - Enables sandboxing. If not set, the app is considered full trust
//
// APPDOMAIN_IGNORE_UNHANDLED_EXCEPTION
// - Prevents the application from being torn down if a managed exception is unhandled
//
APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS |
APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP |
APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT,
NULL, // Name of the assembly that contains the AppDomainManager implementation
NULL, // The AppDomainManager implementation type name
propertyCount,
propertyKeysW,
propertyValuesW,
(DWORD *)domainId);
if (SUCCEEDED(hr))
{
host.SuppressRelease();
*hostHandle = host;
}
return hr;
}
execute_assembly
マクロによるお膳立てコードや、エラーハンドルコードが大量にありますが、バッサリ省略してあります。
重要な部分のみ見ていくと結構シンプルで、ManagedエントリのアセンブリからMain
メソッド(CallSite)を探してきて、引数をStackに積んでCILのCall命令を発行します。
extern "C" DLLEXPORT int coreclr_execute_assembly(
void* hostHandle, unsigned int domainId,
int argc, const char** argv,
const char* managedAssemblyPath, unsigned int* exitCode)
{
*exitCode = -1;
ICLRRuntimeHost4* host = reinterpret_cast<ICLRRuntimeHost4*>(hostHandle);
ConstWStringArrayHolder argvW;
argvW.Set(StringArrayToUnicode(argc, argv), argc);
ConstWStringHolder managedAssemblyPathW = StringToUnicode(managedAssemblyPath);
HRESULT hr = host->ExecuteAssembly(domainId, managedAssemblyPathW, argc, argvW, (DWORD *)exitCode);
IfFailRet(hr);
return hr;
}
HRESULT CorHost2::ExecuteAssembly(
DWORD dwAppDomainId, LPCWSTR pwzAssemblyPath,
int argc, LPCWSTR* argv, DWORD *pReturnValue)
{
AppDomain *pCurDomain = SystemDomain::GetCurrentDomain();
Thread *pThread = GetThreadNULLOk();
Assembly *pAssembly = AssemblySpec::LoadAssembly(pwzAssemblyPath);
pCurDomain->GetMulticoreJitManager().AutoStartProfile(pCurDomain);
// Here we call the managed method that gets the cmdLineArgs array.
SetCommandLineArgs(pwzAssemblyPath, argc, argv);
PTRARRAYREF arguments = NULL;
arguments = (PTRARRAYREF)AllocateObjectArray(argc, g_pStringClass);
for (int i = 0; i < argc; ++i)
{
STRINGREF argument = StringObject::NewString(argv[i]);
arguments->SetAt(i, argument);
}
*pReturnValue = pAssembly->ExecuteMainMethod(&arguments, TRUE /* waitForOtherThreads */);
}
INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads)
{
MethodDesc *pMeth = GetEntryPoint();
RunStartupHooks();
hr = RunMain(pMeth, 1, &iRetVal, stringArgs);
}
HRESULT RunMain(MethodDesc *pFD, short numSkipArgs, INT32 *piRetVal, PTRARRAYREF *stringArgs /*=NULL*/)
{
CorEntryPointType EntryType = EntryManagedMain;
ValidateMainMethod(pFD, &EntryType);
Param param;
param.pFD = pFD;
param.numSkipArgs = numSkipArgs;
param.piRetVal = piRetVal;
param.stringArgs = stringArgs;
param.EntryType = EntryType;
param.cCommandArgs = cCommandArgs;
param.wzArgs = wzArgs;
RunMainInternal(pParam);
}
static void RunMainInternal(Param* pParam)
{
MethodDescCallSite threadStart(pParam->pFD);
// 引数をStackに積む
// Build the parameter array and invoke the method.
// Allocate a COM Array object with enough slots for cCommandArgs - 1
StrArgArray = (PTRARRAYREF) AllocateObjectArray((pParam->cCommandArgs - pParam->numSkipArgs), g_pStringClass);
// Create Stringrefs for each of the args
for (DWORD arg = pParam->numSkipArgs; arg < pParam->cCommandArgs; arg++) {
STRINGREF sref = StringObject::NewString(pParam->wzArgs[arg]);
StrArgArray->SetAt(arg - pParam->numSkipArgs, (OBJECTREF) sref);
}
ARG_SLOT stackVar = ObjToArgSlot(StrArgArray);
// Set the return value to 0 instead of returning random junk
*pParam->piRetVal = 0;
threadStart.Call(&stackVar); // ILのCall命令を発行(多分)
}
まとめ
今回は、ホスト部分(起動部分)を中心に見ていきました。
ランタイムの起動までに起こっている処理が何となく感じられたならと思います。
ランタイムの機能としては他にも、JITやGC、VM、アセンブリ内のTypeの管理などのパートがあり、今回見たのはほんの片鱗にすぎません。
それらも好奇心をそそられますので、そのうち読んでみたいところです。