How the dotnet CLI tooling runs your code
04 Jul 2016 - 1131 wordsJust 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:
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:
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:
- 99 Managed dlls (“Adding runtime asset..”)
- 136 Native dlls (“Adding native asset..”)
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
- Paths to 235 .dlls (99 managed, 136 native), from
- 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:
- Refactor the Unix hosting API
- Expose the Unix hosting API on Windows too
- Expose Unix hosting API on Windows
- Unix Hosting API
And that’s it, your .NET code is now running, simple really!!