.NET 6 runs everywhere, from Windows to the Web, Linux, and mobile and embedded devices. But how? How do they manage to get the same code to run and behave in (mostly) the same way not only across platforms but also across CPU architectures? The secret is in the underlying architecture of .NET 6.
There have been numerous iterations in Microsoft’s cross-platform strategy. We’ve seen shared projects in Xamarin, where the code gets compiled into each platform; we have had portable class libraries where the libraries supported the lowest common denominator of all the selected platforms and more recently we had .NET Standard libraries. But why all these different approaches? It’s actually quite simple. .NET on one platform was not exactly the same as .NET on another platform. We’ve had .NET, Mono, .NET Compact Framework, .NET Micro Framework, etc.
Fixing the splintering of .NET versions was one of the core promises of .NET Core; it took a bit longer than expected but we are finally getting really close to one .NET. No matter what platform you are running on, if your application is running on .NET 6, you can use .NET 6 class libraries and share them over all supported platforms.
.NET 6 Architecture
What the image portraits is the .NET abstraction layer. We write the same .NET code everywhere, but depending on the compile target, a different compiler will be used. When executing a .NET application, a different runtime may be used depending on the platform it is being executed on. Let’s take a command line application, for example, a command line has no UI so no platform-specific code to render screens is necessary, meaning that the same CLI application can run on Windows, Linux, and macOS. When compiling this application, the default .NET 6 compiler will be used, resulting in one executable. Running this executable on Windows will be handled by the common language runtime, CoreCLR. On macOS and Linux, however, this will be handled by Mono, completely transparent to developers and users.
Runtimes
The .NET languages are managed languages, meaning that code you write in C# gets compiled down to intermediate language. Once your code gets executed, that intermediate language is compiled into machine code by the just in time compiler, or JIT. That JIT is part of the common language runtime, or CLR.
When writing .NET code, we don’t program against an operating system; the system APIs in C# don’t target Windows/Linux/macOS directly; instead, they target the API surface of the common language runtime called CoreFX . CoreFX is the newer name of what used to be the Base Class Library or BCL. It includes the System.* namespaces that we use all the time to call platform or framework APIs. The CLR calls into the operating system’s APIs via CoreFX to perform the tasks requested by the developer. In this way, the CLR functions as an abstraction layer, enabling cross-platform code.
The CLR also gives us memory management, keeping track of objects in memory and releasing them when they are no longer needed. This garbage collection is part of the runtime and is what makes .NET languages managed, compared to unmanaged languages like C and C++ where you must do your own memory management.
.NET 6 contains two default runtimes. Depending on the platform you are running your code on, it will be executed by either CoreCLR or Mono.
The .NET 6 runtimes are open source and available at https://github.com/dotnet/runtime.
CoreCLR
The CoreCLR is the .NET 6 version of the classic CLR. It is the common language runtime used for running .NET code on Windows. No matter if it is a desktop application, web application, or console app, if any of these run on Windows, they will use the CoreCLR.
Mono
Mono started as an open-source project to bring .NET and its languages to Linux. Mono was based on the publication of the .NET open standard. The first version of Mono was released in 2004. The maintainers of the Mono open-source project were a small company called Ximian. Ximian and thus Mono were acquired by Novell, Novell was acquired by Attachmate, and the future of Mono seemed very dark. Some people from Ximian formed a new company called Xamarin. Xamarin continued the work on Mono, eventually releasing a mobile cross-platform framework based on Mono. Microsoft became the owner of the Mono project after acquiring Xamarin in 2016.
Mono currently ships as part of .NET 6; it is the default runtime when not running on a Windows-based operating system.
WinRT
The Windows Runtime , or WinRT, is the runtime used for Universal Windows Platform Applications, or UWP. UWP was originally meant to deliver a “build once, run on all Windows 10 devices.” These devices included computers, tablets, smartphones, Xbox, Hololens, and embedded devices. WinRT applications can be built using C# or C++ and XAML. WinRT is not a runtime in the strict sense of the word. It’s more like an interface on top of the Win32 API.
Managed Execution Process
First step is compiling to the Microsoft Intermediate Language, or MSIL. For this, we will need a compiler that can compile the language we’re writing our code in to intermediate language.
The second step is compiling the MSIL code into native code. There are two ways to do this.
The first one is using the Just-In-Time, or JIT compiler. The JIT compiler is supplied by the runtime, making JIT compilation possible on different architectures and operating system. If there is a .NET runtime on the platform, there is a JIT compiler. JIT compilation is not a one-shot process; it happens continuously as your application is being used; this is by design to keep in mind that not all code in the MSIL will end up being called. By JIT compiling on the go, the runtime limits the number of resources your application is using. Once a piece of MSIL is compiled into native code, it is stored in memory and does not need to recompile if the application is running.
An important step in the compilation step of the managed execution process for both JIT and AOT is code verification. Code verifications makes sure that the code being compiled into native is safe; it protects the system from malicious behavior in software. The compiler takes the MSIL and treats it as unsafe by default. It will verify that the MSIL was correctly generated, that no memory locations can be accessed that shouldn’t be accessed, that all type references are compatible, and so on. Note that this verification can be disabled by system administrator.
The final step in the managed execution process is running the code. This is where the operating system takes the native code, either from the AOT compiler or from the JIT compiler, and executes the instructions. While the application is being executed, the runtime will trigger services like garbage collection, code verification, and so on.
Desktop Packs
On the image we can clearly see that .NET is still very much cross-platform, but should we want to add Windows-only code, for example, we can by referencing a specific .NET implementation through a Target Framework Moniker, or TFM.
Adding WPF support
Adding WinForms support
net6.0
net6.0-Android
net6.0-ios
net6.0-macos
net6.0-maccatalyst
net6.0-tvos
net6.0-Windows
Creating a new WPF or WinForms project will automatically set the TFM to net6.0-windows.
The WPF and WinForms project templates include a reference to Microsoft.WindowsDesktop.App.WPF or Microsoft.WindowsDesktop.App.WinForms. These are called Desktop Packs.
While the net6.0-windows TFM is sufficient to get access to the native Windows APIs, it does not contain the specific logic to render WinForms via GDI+ or WPF via DirectX. That logic is contained in the desktop packs. We go over how WinForms and WPF work in more detail in Chapter 4 of this book.
Wrapping Up
While .NET 6 is an easy-to-use and very developer-friendly framework, there is a lot going on under the hood. It has a layered architecture with several runtimes, a complex three-step compilation process, and even different ways of compiling. All of this complexity is hidden pretty well for us developers; we don’t have to worry that our application will select the Mono runtime on Linux; that is all taken care of for us. However, it is still important to have an idea of what is going on under the hood.
Besides making .NET easy to use, Microsoft had a big challenge with maintaining the cross-platform dream while still being able to provide access to platform-native APIs. Not only for new applications written in new technologies but also for more mature frameworks like WPF and WinForms. Multiple extensions of .NET 6 were created to solve this. We can use these extensions by targeting the correct Target Framework Moniker. Add the desktop packs to this and the cross-platform, native story with full legacy support is complete.