How the dotnet CLI tooling runs your code

Just over a week ago the official 1.0 release of .NET Core was announced, the release includes:

the .NET Core runtime, libraries and tools and the ASP.NET Core libraries.

However alongside a completely new, revamped, xplat version of the .NET runtime, the development experience has been changed, with the dotnet based tooling now available (Note: the tooling itself is currently still in preview and it’s expected to be RTM later this year)

So you can now write:

dotnet new
dotnet restore
dotnet run

and at the end you’ll get the following output:

Hello World!

It’s the dotnet CLI (Command Line Interface) tooling that is the focus of this post and more specifically how it actually runs your code, although if you want a tl;dr version see this tweet from @citizenmatt:

Tweet explaining dotnet CLI runtime


Traditional way of running .NET executables

As a brief reminder, .NET executables can’t be run directly (they’re just IL, not machine code), therefore the Windows OS has always needed to do a few tricks to execute them, from CLR via C#:

After Windows has examined the EXE file’s header to determine whether to create a 32-bit process, a 64-bit process, or a WoW64 process, Windows loads the x86, x64, or IA64 version of MSCorEE.dll into the process’s address space. … Then, the process’ primary thread calls a method defined inside MSCorEE.dll. This method initializes the CLR, loads the EXE assembly, and then calls its entry point method (Main). At this point, the managed application is up and running.

New way of running .NET executables

dotnet run

So how do things work now that we have the new CoreCLR and the CLI tooling? Firstly to understand what is going on under-the-hood, we need to set a few environment variables (COREHOST_TRACE and DOTNET_CLI_CAPTURE_TIMING) so that we get a more verbose output:

dotnet run - with cli timings and verbose output

Here, amongst all the pretty ASCII-art, we can see that dotnet run actually executes the following cmd:

dotnet exec --additionalprobingpath C:\Users\matt\.nuget\packages c:\dotnet\bin\Debug\netcoreapp1.0\myapp.dll

Note: this is what happens when running a Console Application. The CLI tooling supports other scenarios, such as self-hosted web sites, which work differently.

dotnet exec and corehost

Up-to this point everything was happening within managed code, however once dotnet exec is called we jump over to unmanaged code within the corehost application. In addition several other .dlls are loaded, the last of which is the CoreCLR runtime itself (click to go to the main source file for each module):

The main task that the corehost application performs is to calculate and locate all the dlls needed to run the application, along with their dependencies. The full output is available, but in summary it processes:

There are so many individual files because the CoreCLR operates on a “pay-for-play” model, from Motivation Behind .NET Core:

By factoring the CoreFX libraries and allowing individual applications to pull in only those parts of CoreFX they require (a so-called “pay-for-play” model), server-based applications built with ASP.NET 5 can minimize their dependencies.

Finally, once all the housekeeping is done control is handed off to corehost, but not before the following properties are set to control the execution of the CoreCLR itself:

  • TRUSTED_PLATFORM_ASSEMBLIES =
    • Paths to 235 .dlls (99 managed, 136 native), from C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.0-rc2-3002702
  • APP_PATHS =
    • c:\dotnet\bin\Debug\netcoreapp1.0
  • APP_NI_PATHS =
    • c:\dotnet\bin\Debug\netcoreapp1.0
  • NATIVE_DLL_SEARCH_DIRECTORIES =
    • C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.0-rc2-3002702
    • c:\dotnet\bin\Debug\netcoreapp1.0
  • PLATFORM_RESOURCE_ROOTS =
    • c:\dotnet\bin\Debug\netcoreapp1.0
    • C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.0-rc2-3002702
  • AppDomainCompatSwitch =
    • UseLatestBehaviorWhenTFMNotSpecified
  • APP_CONTEXT_BASE_DIRECTORY =
    • c:\dotnet\bin\Debug\netcoreapp1.0
  • APP_CONTEXT_DEPS_FILES =
    • c:\dotnet\bin\Debug\netcoreapp1.0\dotnet.deps.json
    • C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.0-rc2-3002702\Microsoft.NETCore.App.deps.json
  • FX_DEPS_FILE =
    • C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.0.0-rc2-3002702\Microsoft.NETCore.App.deps.json

Note: You can also run your app by invoking corehost.exe directly with the following command:

corehost.exe C:\dotnet\bin\Debug\netcoreapp1.0\myapp.dll

Executing a .NET Assembly

At last we get to the point at which the .NET dll/assembly is loaded and executed, via the code shown below, taken from unixinterface.cpp:

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);

This is making use of the ICLRRuntimeHost Interface, which is part of the COM based hosting API for the CLR. Despite the file name, it is actually from the Windows version of the CLI tooling. In the xplat world of the CoreCLR the hosting API that was originally written for Unix has been replicated across all the platforms so that a common interface is available for any tools that want to use it, see the following GitHub issues for more information:

And that’s it, your .NET code is now running, simple really!!


Additional information: