C# Assemblies, compiler and everything in between

Abdulmalik
6 min readMar 30, 2023

--

Photo by Andre Benz on Unsplash

Assemblies, runtime environments, compilers are interesting concepts in programming languages; At least, maybe not for the developers but they are at the heart of how lots of programming languages work and how they achieve platform independency. The .NET Framework allows you build and run the same language (or a family of languages) across different platforms and architectures, as long as you have the right set of compilers.

For instance, Java - a high level language, runs on different platforms by compiling higher level language into an intermediate language known as Bytecode. C# and F# are also compiled into a Common Intermediate Language (CIL), which are stored as assemblies.

Basically, Assemblies allow you manage IL codes that can be translated into native code by the Common Language Runtime either at runtime (Just-in-time) or after build (Ahead-of-time).

In this post, I’ll discuss the fundamentals of how C# Assemblies work under the hood, its importance and we’ll take a slice of an interesting concept called Reflection.

1. What are assemblies?

Assemblies are a collection of types and resources that are built to work together and form a logical unit of functionality. They usually compose of one or more compiled source code files or modules and therefore, they are considered the building blocks of C# applications.

You can think of assemblies as the basic unit of deployment, version control, reuse, activation scoping, and security permissions for .NET-based applications.

Assemblies containing an execution entry-point (Main method) are referred to as EXE and they are executable on your Windows host. On the other hand, we have DLLs ( Dynamic Link Libraries), which you can utilize within your source code and they are dynamically compiled during runtime.

So why do we need assemblies?

Assemblies provide the common language runtime with the information it needs to be aware of type implementations. What makes assemblies powerful is that, every assembly is executed within its managed context/unit. Assemblies are considered as a single manageable unit and therefore they can easily be used to ship versioned, compact and maintainable parts of a program.

2. NuGet Packages vs Assemblies

One of those questions that I struggled with while studying assemblies in C# is: “Since we have assemblies, why do we need packages?”

Packages and Package managers are essential tools that enable developers share, maintain and consume code either from different hosts (sources). C#’s package manager NuGet, allows you to access packages from the public NuGet domain (nuget.org) or within your private organization or work group.

Packages contain manifests describing the package version, icons, package name, etc and also one or more assemblies (DLLs) that has your compiled code. The image below describes how you can create a package and have it shipped to the PCs of other developers/consumers:

From Microsoft c#

Other popular package managers for other languages include Composer (PHP), NPM (Javascript), Maven (Java), etc.

3. Structure of an Assembly in C#

An assembly generally consists of 4 elements: Assembly Manifest, Type Metadata, MSIL (Microsoft Intermediate Language) Code and Resource (bitmaps, xml, assets).

Assembly Manifest: The assembly manifest performs the functions of a regular manifest file, which is to provide details about a project/program. In C#, it also makes the assembly self-describing as it contains data about the assembly such as version requirements, references to classes and libraries, security identity, metadata for assembly dependencies, etc.

Some metadata from a sample HelloWorld assembly

You might want to read up Microsoft’s documentation for a comprehensive list of contents in the assembly manifest.

Type Metadata: The type metadata contains relevant data to describe the Types within the assembly and allow the assembly map implementations those types. For instance if you have a class, interface or enum MyClass in your program, which type is considered as MyClass, then it is assumed that this type would be described by metadata in the compiled assembly.

MSIL (Microsoft Intermediate Language) Code: The MSIL code, as you’ve imagined is just boring. C# is and the rest of the pack (F#, Visual Basic, etc) are compiled into a unified language which would subsequently be translated into machine code. This mode of translation is determined by the compiler as it can be JIT (during execution) or it is readily available (AOT). You can view the IL (Intermediate Language) and the CLR (Common Language Runtime) in C# as somewhat similar to Bytecodes and JRE (Java Runtime Environment) in Java.

IL Code for sample HelloWorld source code

See why I said it’s boring?☝🏽

From Geeksforgeeks

This architecture enables these languages to function independently on multiple platforms and architectures, as long as there’s an available runtime environment for that platform. The diagram above would become meaningful as we discuss the CLR, runtime environment and execution process in the rest of this post.

Resource (bitmaps, xml, assets): Assemblies can contain various types of resources like images, icons, xml files, etc. Resources are mostly static for their lifetime, i.e.; they hardly change during runtime. Depending on your deployment strategy, resources can be packaged with assemblies in a single file or the assembly can comprise of multiple files (multifile assemblies). A typical resource example could be an XML file containing data that can be queried.

4. The CLR (Common Language Runtime)

C#’s .NET framework provides a run-time environment called the common language runtime. The CLR runs the code and provides services that make the development process easier, so you don’t have to worry about the process of running cross-platform native code.

The runtime utilizes metadata (remember😁) to locate and load classes, lay out instances in memory, resolve method invocations, generate native code, enforce security, and set run-time context boundaries. It also automatically releases references to objects when they’re no longer being used and performs garbage collection to eliminate memory leaks. Because the CLR targets native code, it also comes along with huge performance boosts.

CLR Architecture From Javatpoint

Another cool thing about the CLR is that you can tightly integrate different languages (CLS Compliant languages — F#, C#, Visual Basic), and have them operate on a common type system — defined by the runtime, considering that your compilers/tools target the same runtime. In reality, this means I can pass an instance of a class created in F# to a method of a class written in C#.

The functions of the CLR also includes:

  • The ability to easily use components developed in other languages (remember?).
  • Language features such as inheritance, interfaces, and overloading for object-oriented programming.
  • Support for explicit free threading that allows creation of multithreaded and scalable applications.
  • Support for structured exception handling.
  • You can explore other features and functions of the CLR from Microsoft’s documentations.

6. The Side by Side Execution Model

Side by side simply means I can run multiple independent versions of my program at the same time without conflicts.

.NET allows you to run multiple versions of the common language runtime and even multiple versions of applications & components that use a version of the runtime, on the same computer at the same time. For example, version 4.0 of the runtime is actually version 4.0.319, while version 1.0 of the .NET Framework assemblies is version 1.0.3300.0, and they can operate simultaneously because they have some form of uniqueness.

Do you remember assemblies have unique Version Names? Here are some features that enables the side-by-side model to work:

  • Strong named assemblies: Side-by-side execution uses strong-named assemblies to bind type information to a specific version of an assembly. This prevents an application or component from binding to an invalid version of an assembly. See how to sign an assembly with Strong-Names.
  • Isolation: Isolation is an essential to achieve such execution. It involves an assembly being aware of resources it’s using and sharing resources with confidence among multiple versions of an application or component. Isolation also includes storing files in a version-specific way.

I believe this covers the basic concepts you’d need to do a deeper research and get better at your craft. I’ll be discussing Reflection very soon.

Thanks for reading. Happy coding!

--

--

Abdulmalik

I've recently embraced the culture of reading, since I discovered it's equivalent to hundreds of mentors.