Conceptual Docs

How do we make C# work at low-level?


As I'm sure you are aware, C# is not a low-level language. So making it work as a language for OS development seems like both a crazy and impossible idea. However, we've shown in our Why C# article that there are a fair few good reasons for doing it. In this article, I'll briefly discuss the key technical problems and solutions for making C# work as a low-level language.

1. C# compiles to IL code not machine code

Yes, MSBuild converts C# into IL code not machine code and then when you run a C# program, JIT compiles it to machine code. Our solution is to write our own Ahead-of-Time compiler. This is gives us the big advantage (for teaching purposes) that even our machine code is very readable and you can trace right from C# to IL to machine code. No obfuscation through the compiler!

Our compiler supports MIPS and x86 so is cross-platform. We are planning to expand our MIPS kernel significantly in 2016.

There are one or two compiler limitations that we are working on at the moment:

  1. We don't support interfaces (yet)
  2. We don't support generics (we are reviewing if they are necessary or not)
  3. We probably don't support some other advanced features that we haven't tested (e.g. lambda functions) may or may not work. We don't anticipate supporting these.

2. C# has native pointers

Yes, C# has native pointers. Turn on Unsafe code in the project options, apply the "unsafe" keyword to classes/methods/blocks and you have everything you need.

3. What about the .NET Framework?

In short, we don't use or support it. Calls to the .NET Framework are ignored by the compiler (which new developers may find causes them a few confusing issues - we're working hard to smooth this out). We have our own Object class, from which every class must (directly or indirectly) inherit. We also have our own Type, String, Array and Delegate classes which need to be used instead of the ones from the System library. Automatic conversion from System.String to FOS_System.String is built-in. Our Type class also has associated structures giving us full reflection capability.

4. What about managed memory?

We have our own Garbage Collector class (which is a fairly simple ref-count based implementation - simple is easier to teach with). Calls to the Garbage Collector are automatically inserted by the compiler (but this can be disabled for individual methods for e.g. inside critical interrupt handlers).

5. What about raw assembly code? Inline assembly?

We have a plugging mechanism which uses C# attributes to specify a replacement assembly code file for a method. Instead of using the IL/ASM code for the method generated by the compiler, it substitutes whatever is in the assembly code file. If the calling convention is followed, these hand-coded assembly methods can be called from other C# functions as normal. A prioritising attribute can also be applied to methods to specify where in the output machine code they appear. This allows specific assembly code to be placed at the start of the OS machine code to handle the start from a bootloader.

6. Pointers and objects

We don't use references in the formal sense. Object references (i.e. variables of type object) are just native pointers to objects and likewise delegates are just native pointers to methods (which can be called directly but only static methods can be referred to at the moment). This means, through a small assembly code plug, you can convert from any object to a pointer and back again which is very useful when writing low level code. However, it does break away from Microsoft's C# slightly but this does particularly affect developers who are used to Microsoft C#.

7. Surely there is more to it than that?

Yes, there are plenty more detailed points which we could discuss (most of which affect only the compiler - not expected to be taught) but from an OS dev point of view, this covers pretty much everything.